├── README.md ├── cloud ├── .firebaserc ├── .gitignore ├── firebase.json ├── firestore.indexes.json ├── firestore.rules └── functions │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── constants.ts │ ├── handlers.ts │ ├── index.ts │ ├── pinger_store.ts │ └── types.ts │ ├── tsconfig.json │ └── tslint.json └── mobile ├── .github └── workflows │ ├── internal_android.yml │ ├── internal_ios.yml │ ├── release_android.yml │ ├── release_ios.yml │ └── run_tests.yml ├── .gitignore ├── .metadata ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── Gemfile ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── dev │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ └── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── io │ │ │ │ └── flutter │ │ │ │ └── app │ │ │ │ └── FlutterMultiDexApplication.java │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── tomwyr │ │ │ │ └── pinger │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── splash.png │ │ │ ├── drawable-mdpi │ │ │ └── splash.png │ │ │ ├── drawable-xhdpi │ │ │ └── splash.png │ │ │ ├── drawable-xxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── splash.png │ │ │ ├── drawable │ │ │ ├── ic_notification.png │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── prod │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ └── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── fastlane │ ├── Appfile │ ├── Fastfile │ └── Pluginfile ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── build.yaml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Gemfile ├── Gemfile.lock ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ ├── dev.xcscheme │ │ └── prod.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon-dev.appiconset │ │ │ ├── AppIcon-dev-1024x1024@1x.png │ │ │ ├── AppIcon-dev-20x20@1x.png │ │ │ ├── AppIcon-dev-20x20@2x.png │ │ │ ├── AppIcon-dev-20x20@3x.png │ │ │ ├── AppIcon-dev-29x29@1x.png │ │ │ ├── AppIcon-dev-29x29@2x.png │ │ │ ├── AppIcon-dev-29x29@3x.png │ │ │ ├── AppIcon-dev-40x40@1x.png │ │ │ ├── AppIcon-dev-40x40@2x.png │ │ │ ├── AppIcon-dev-40x40@3x.png │ │ │ ├── AppIcon-dev-60x60@2x.png │ │ │ ├── AppIcon-dev-60x60@3x.png │ │ │ ├── AppIcon-dev-76x76@1x.png │ │ │ ├── AppIcon-dev-76x76@2x.png │ │ │ ├── AppIcon-dev-83.5x83.5@2x.png │ │ │ └── Contents.json │ │ ├── AppIcon-prod.appiconset │ │ │ ├── AppIcon-prod-1024x1024@1x.png │ │ │ ├── AppIcon-prod-20x20@1x.png │ │ │ ├── AppIcon-prod-20x20@2x.png │ │ │ ├── AppIcon-prod-20x20@3x.png │ │ │ ├── AppIcon-prod-29x29@1x.png │ │ │ ├── AppIcon-prod-29x29@2x.png │ │ │ ├── AppIcon-prod-29x29@3x.png │ │ │ ├── AppIcon-prod-40x40@1x.png │ │ │ ├── AppIcon-prod-40x40@2x.png │ │ │ ├── AppIcon-prod-40x40@3x.png │ │ │ ├── AppIcon-prod-60x60@2x.png │ │ │ ├── AppIcon-prod-60x60@3x.png │ │ │ ├── AppIcon-prod-76x76@1x.png │ │ │ ├── AppIcon-prod-76x76@2x.png │ │ │ ├── AppIcon-prod-83.5x83.5@2x.png │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── Runner-Bridging-Header.h │ └── SimplePing │ │ ├── SimplePing.h │ │ ├── SimplePing.m │ │ └── SimplePingChannel.swift ├── fastlane │ ├── Fastfile │ └── Pluginfile └── firebase_app_id_file.json ├── lib ├── assets.dart ├── assets │ ├── icons │ │ ├── app-icon-dev.png │ │ └── app-icon-prod.png │ └── images │ │ ├── splash-hd.png │ │ ├── splash.png │ │ ├── undraw_collecting.png │ │ ├── undraw_empty.png │ │ ├── undraw_road_sign.png │ │ ├── undraw_runner_start.png │ │ ├── undraw_searching.png │ │ ├── undraw_server_down.png │ │ ├── undraw_settings.png │ │ ├── undraw_signal_searching.png │ │ ├── undraw_the_world_is_mine.png │ │ └── undraw_void.png ├── config.dart ├── di │ ├── injector.config.dart │ └── injector.dart ├── extensions.dart ├── firebase_options.dart ├── generated │ ├── intl │ │ ├── messages_all.dart │ │ ├── messages_en.dart │ │ ├── messages_fr.dart │ │ └── messages_pl.dart │ └── l10n.dart ├── l10n │ ├── intl_en.arb │ ├── intl_fr.arb │ └── intl_pl.arb ├── main.dart ├── model │ ├── geo_position.dart │ ├── geo_position.freezed.dart │ ├── geo_position.g.dart │ ├── host_stats.dart │ ├── host_stats.freezed.dart │ ├── host_stats.g.dart │ ├── ping_global.dart │ ├── ping_global.freezed.dart │ ├── ping_global.g.dart │ ├── ping_result.dart │ ├── ping_result.freezed.dart │ ├── ping_result.g.dart │ ├── ping_session.dart │ ├── ping_session.freezed.dart │ ├── user_settings.dart │ ├── user_settings.freezed.dart │ └── user_settings.g.dart ├── resources.dart ├── service │ ├── favicon_service.dart │ ├── notifications_manager.dart │ ├── ping_service.dart │ ├── pinger_api.dart │ ├── pinger_prefs.dart │ └── vibration.dart ├── store │ ├── device_store.dart │ ├── device_store.g.dart │ ├── hosts_store.dart │ ├── hosts_store.g.dart │ ├── permission_store.dart │ ├── permission_store.g.dart │ ├── ping_store.dart │ ├── ping_store.g.dart │ ├── results_store.dart │ ├── results_store.g.dart │ ├── settings_store.dart │ └── settings_store.g.dart ├── ui │ ├── app │ │ ├── pinger_app.dart │ │ └── pinger_router.dart │ ├── common │ │ ├── animated_ink_icon.dart │ │ ├── box_clipper.dart │ │ ├── collapsing_header.dart │ │ ├── collapsing_header_delegate.dart │ │ ├── collapsing_tab_layout.dart │ │ ├── collapsing_tile.dart │ │ ├── dotted_map.dart │ │ ├── draggable_sheet.dart │ │ ├── fade_out.dart │ │ ├── flex_child_scroll_view.dart │ │ ├── inline_multi_child_layout_delegate.dart │ │ ├── scroll_edge_gradient.dart │ │ ├── separated_sliver_list.dart │ │ ├── snapping_scroll_area.dart │ │ └── transparent_gradient_box.dart │ ├── info_tray │ │ ├── info_tray.dart │ │ ├── info_tray_entry.dart │ │ ├── info_tray_sheet.dart │ │ └── items │ │ │ ├── info_tray_connectivity_item.dart │ │ │ └── info_tray_session_item.dart │ ├── page │ │ ├── archive_page.dart │ │ ├── base_page.dart │ │ ├── favorites_page.dart │ │ ├── home │ │ │ ├── home_host_suggestions.dart │ │ │ ├── home_hosts_section.dart │ │ │ └── home_page.dart │ │ ├── hosts_page.dart │ │ ├── init_page.dart │ │ ├── intro_page.dart │ │ ├── recents_page.dart │ │ ├── result_details │ │ │ ├── result_details_header.dart │ │ │ ├── result_details_page.dart │ │ │ ├── result_details_summary_chart.dart │ │ │ └── result_details_tab │ │ │ │ ├── result_details_global_tab.dart │ │ │ │ ├── result_details_info_tab.dart │ │ │ │ ├── result_details_more_tab.dart │ │ │ │ ├── result_details_prompt_tab.dart │ │ │ │ └── result_details_results_tab.dart │ │ ├── search_page.dart │ │ ├── session │ │ │ ├── session_button │ │ │ │ ├── ping_button.dart │ │ │ │ ├── ping_floating_button.dart │ │ │ │ └── ping_lock_indicator.dart │ │ │ ├── session_host_button.dart │ │ │ ├── session_host_header.dart │ │ │ ├── session_page.dart │ │ │ ├── session_ping_gauge.dart │ │ │ ├── session_summary_section.dart │ │ │ └── session_values │ │ │ │ ├── session_values_chart.dart │ │ │ │ ├── session_values_item.dart │ │ │ │ ├── session_values_list.dart │ │ │ │ ├── session_values_scrollable.dart │ │ │ │ └── session_values_section.dart │ │ └── settings │ │ │ ├── settings_items.dart │ │ │ ├── settings_page.dart │ │ │ └── settings_sections.dart │ ├── permissions_sheet.dart │ └── shared │ │ ├── chart │ │ ├── global_distribution_chart.dart │ │ ├── ping_results_chart.dart │ │ ├── result_summary_chart.dart │ │ └── result_tile_chart.dart │ │ ├── info_section.dart │ │ ├── session │ │ └── session_values_section.dart │ │ ├── sheet │ │ ├── pinger_bottom_sheet.dart │ │ └── replace_session_sheet.dart │ │ ├── three_bounce.dart │ │ ├── tiles │ │ ├── host_icon_tile.dart │ │ ├── host_tile.dart │ │ ├── result_tile.dart │ │ └── results_group_tile.dart │ │ └── view_type │ │ ├── view_type_button.dart │ │ ├── view_type_row.dart │ │ └── view_types.dart └── utils │ ├── data_snap.dart │ ├── data_snap.freezed.dart │ ├── format_utils.dart │ ├── host_tap_handler.dart │ ├── lifecycle_notifier.dart │ └── notification_messages.dart ├── pubspec.lock ├── pubspec.yaml ├── r_flutter.build.yaml ├── test └── extensions_test.dart └── web └── index.html /README.md: -------------------------------------------------------------------------------- 1 | # Pinger 2 | 3 | Ping command utility app made with Flutter. 4 | 5 | 6 | 7 | ### Used libraries, tools and services: 8 | - (some) community packages: 9 | - [mobx](https://pub.dev/packages/mobx) 10 | - [freezed](https://pub.dev/packages/freezed) 11 | - [injectable](https://pub.dev/packages/injectable) 12 | - [fl_chart](https://pub.dev/packages/fl_chart) 13 | - [Firebase](https://github.com/FirebaseExtended/flutterfire) 14 | - Firestore 15 | - App Distribution 16 | - Crashlytics 17 | - [Fastlane](https://docs.fastlane.tools/) 18 | - [GitHub Actions](https://help.github.com/en/actions) 19 | -------------------------------------------------------------------------------- /cloud/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "pinger-855aa", 4 | "dev": "pinger-dev-86e02" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /cloud/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | # .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | -------------------------------------------------------------------------------- /cloud/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "functions": { 7 | "predeploy": [ 8 | "npm --prefix \"$RESOURCE_DIR\" run lint", 9 | "npm --prefix \"$RESOURCE_DIR\" run build" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cloud/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /cloud/firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | 5 | // This rule allows anyone on the internet to view, edit, and delete 6 | // all data in your Firestore database. It is useful for getting 7 | // started, but it is configured to expire after 30 days because it 8 | // leaves your app open to attackers. At that time, all client 9 | // requests to your Firestore database will be denied. 10 | // 11 | // Make sure to write security rules for your app before that time, or else 12 | // your app will lose access to your Firestore database 13 | match /{document=**} { 14 | allow read, write: if request.time < timestamp.date(2020, 4, 28); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /cloud/functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Typescript v1 declaration files 6 | typings/ 7 | 8 | node_modules/ -------------------------------------------------------------------------------- /cloud/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase emulators:start --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "10" 14 | }, 15 | "main": "lib/index.js", 16 | "dependencies": { 17 | "firebase-admin": "^11.4.1", 18 | "firebase-functions": "^3.11.0" 19 | }, 20 | "devDependencies": { 21 | "firebase-functions-test": "^0.1.6", 22 | "tslint": "^5.12.0", 23 | "typescript": "^3.9.7" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /cloud/functions/src/constants.ts: -------------------------------------------------------------------------------- 1 | export class Collections { 2 | static readonly countsDaily = "counts-daily"; 3 | static readonly countsMonthly = "counts-monthly"; 4 | static readonly resultsDaily = "results-daily"; 5 | static readonly resultsMonthly = "results-monthly"; 6 | static readonly sessions = "sessions"; 7 | } 8 | 9 | export class Paths { 10 | static readonly all = "all"; 11 | static readonly session = "sessions/{id}"; 12 | } 13 | 14 | export class Regions { 15 | static readonly euWest3 = "europe-west3"; 16 | } 17 | 18 | export class Intervals { 19 | static readonly secondsPerDay = 24 * 60 * 60 * 1000; 20 | static readonly monthlyDataDaysSpan = 30; 21 | static readonly refreshDataInterval = "every 24 hours"; 22 | } 23 | -------------------------------------------------------------------------------- /cloud/functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import admin = require("firebase-admin"); 2 | import functions = require("firebase-functions"); 3 | import handlers = require("./handlers"); 4 | import { Intervals, Paths, Regions } from "./constants"; 5 | import { Session } from "./types"; 6 | 7 | admin.initializeApp(); 8 | 9 | exports.updateDailyData = functions 10 | .region(Regions.euWest3) 11 | .firestore.document(Paths.session) 12 | .onCreate((snapshot, _) => { 13 | const session = snapshot.data() as Session; 14 | return Promise.all([ 15 | handlers.updateDailyCounts(session), 16 | handlers.updateDailyResults(session), 17 | ]); 18 | }); 19 | 20 | exports.resfreshMonthlyData = functions 21 | .region(Regions.euWest3) 22 | .pubsub.schedule(Intervals.refreshDataInterval) 23 | .onRun((_) => 24 | Promise.all([ 25 | handlers.refreshMonthlyCounts(), 26 | handlers.refreshMonthlyResults(), 27 | ]) 28 | ); 29 | -------------------------------------------------------------------------------- /cloud/functions/src/pinger_store.ts: -------------------------------------------------------------------------------- 1 | import admin = require("firebase-admin"); 2 | import { Collections, Paths } from "./constants"; 3 | import { DailyCounts, DailyResults, JsonObject, MonthlyCounts, MonthlyResults } from "./types"; 4 | 5 | export class PingerStore { 6 | _firestoreInstance!: FirebaseFirestore.Firestore; 7 | 8 | public get _firestore() { 9 | return ( 10 | this._firestoreInstance || (this._firestoreInstance = admin.firestore()) 11 | ); 12 | } 13 | 14 | public get currentDate(): Date { 15 | return admin.firestore.Timestamp.now().toDate(); 16 | } 17 | 18 | async getDailyCounts(): Promise { 19 | const dailyCountsSnap = await this._firestore 20 | .collection(Collections.countsDaily) 21 | .doc(Paths.all) 22 | .get(); 23 | return dailyCountsSnap.data() ?? {}; 24 | } 25 | 26 | async setDailyCounts(dailyCounts: DailyCounts) { 27 | await this._firestore 28 | .collection(Collections.countsDaily) 29 | .doc(Paths.all) 30 | .set(dailyCounts); 31 | } 32 | 33 | async deleteMonthlyResults(host: string) { 34 | await this._firestore 35 | .collection(Collections.resultsMonthly) 36 | .doc(host) 37 | .delete(); 38 | } 39 | 40 | async getDailyResults(host: string): Promise { 41 | const dailyResultsSnap = await this._firestore 42 | .collection(Collections.resultsDaily) 43 | .doc(host) 44 | .get(); 45 | return dailyResultsSnap.data() ?? {}; 46 | } 47 | 48 | async setDailyResults(host: string, dailyResults: DailyResults) { 49 | await this._firestore 50 | .collection(Collections.resultsDaily) 51 | .doc(host) 52 | .set(dailyResults); 53 | } 54 | 55 | async deleteDailyResults(host: string) { 56 | await this._firestore 57 | .collection(Collections.resultsDaily) 58 | .doc(host) 59 | .delete(); 60 | } 61 | 62 | async setMonthlyCounts(monthlyCounts: MonthlyCounts) { 63 | await this._firestore 64 | .collection(Collections.countsMonthly) 65 | .doc(Paths.all) 66 | .set(monthlyCounts); 67 | } 68 | 69 | async setMonthlyResults(host: string, monthlyResults: MonthlyResults) { 70 | await this._firestore 71 | .collection(Collections.resultsMonthly) 72 | .doc(host) 73 | .set(monthlyResults); 74 | } 75 | 76 | async getAllDailyResults(): Promise> { 77 | const dailyResultsQuery = await this._firestore 78 | .collection(Collections.resultsDaily) 79 | .get(); 80 | const allDailyResults: JsonObject = {}; 81 | dailyResultsQuery.docs.forEach((it) => { 82 | const host = it.ref.path.split("/").pop()!; 83 | allDailyResults[host] = it.data(); 84 | }); 85 | return allDailyResults; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /cloud/functions/src/types.ts: -------------------------------------------------------------------------------- 1 | export type JsonObject = { [key: string]: T }; 2 | 3 | export type Session = { 4 | host: string; 5 | count: number; 6 | location: GeoPoint; 7 | stats: PingStats; 8 | }; 9 | 10 | export type DailyCounts = JsonObject; 11 | 12 | export type DailyResults = JsonObject; 13 | 14 | export type MonthlyCounts = { 15 | totalCount: number; 16 | pingCounts: Array; 17 | }; 18 | 19 | export type MonthlyResults = { 20 | totalCount: number; 21 | valueResults: ValueResults; 22 | locationResults: Array; 23 | }; 24 | 25 | export type HostCounts = { 26 | totalCount: number; 27 | pingCounts: JsonObject; 28 | }; 29 | 30 | export type HostResults = { 31 | totalCount: number; 32 | valueResults: ValueResults; 33 | locationResults: JsonObject; 34 | }; 35 | 36 | export type PingCount = { 37 | host: string; 38 | count: number; 39 | }; 40 | 41 | export type ValueResults = { 42 | min: JsonObject; 43 | mean: JsonObject; 44 | max: JsonObject; 45 | }; 46 | 47 | export type LocationResults = { 48 | location: GeoPoint; 49 | count: number; 50 | stats: PingStats; 51 | }; 52 | 53 | export type PingStats = { 54 | min: number; 55 | mean: number; 56 | max: number; 57 | }; 58 | 59 | export type GeoPoint = { 60 | lat: number; 61 | lon: number; 62 | }; 63 | -------------------------------------------------------------------------------- /cloud/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2017" 10 | }, 11 | "compileOnSave": true, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /mobile/.github/workflows/internal_android.yml: -------------------------------------------------------------------------------- 1 | name: internal_android 2 | 3 | on: 4 | push: 5 | branches: [dev] 6 | pull_request: 7 | branches: [dev] 8 | 9 | jobs: 10 | build_and_publish_android_to_firebase: 11 | name: Publish Android app for internal testing 12 | runs-on: macOS-latest 13 | env: 14 | APP_ID: com.tomwyr.pinger.dev 15 | APP_FLAVOR: dev 16 | FIREBASE_GROUPS: testers 17 | FIREBASE_TOKEN: ${{secrets.FIREBASE_TOKEN}} 18 | steps: 19 | # Setup environment 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-java@v1 22 | with: 23 | java-version: "12.x" 24 | - uses: subosito/flutter-action@v1 25 | with: 26 | channel: "stable" 27 | - run: npm install -g firebase-tools 28 | - run: flutter pub get 29 | - name: Decode confidential files 30 | env: 31 | FIREBASE_API_KEY_ANDROID: ${{secrets.FIREBASE_API_KEY_ANDROID_DEV}} 32 | run: echo $FIREBASE_API_KEY_ANDROID | base64 -d > android/app/src/dev/google-services.json 33 | # Build and publish Android 34 | - name: Build Android 35 | run: flutter build apk --flavor dev --dart-define APP_ENV=dev 36 | - name: Publish Android 37 | working-directory: ./android 38 | env: 39 | FIREBASE_APP_ID: 1:19662603391:android:d82f1b46633823fabd2b4a 40 | run: fastlane publish_firebase 41 | -------------------------------------------------------------------------------- /mobile/.github/workflows/internal_ios.yml: -------------------------------------------------------------------------------- 1 | name: internal_ios 2 | 3 | on: 4 | push: 5 | branches: [dev] 6 | pull_request: 7 | branches: [dev] 8 | 9 | jobs: 10 | build_and_publish_ios_to_firebase: 11 | name: Publish iOS app for internal testing 12 | runs-on: macOS-latest 13 | env: 14 | APP_ID: com.tomwyr.pinger.dev 15 | APP_FLAVOR: dev 16 | FIREBASE_GROUPS: testers 17 | FIREBASE_TOKEN: ${{secrets.FIREBASE_TOKEN}} 18 | steps: 19 | # Setup environment 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-java@v1 22 | with: 23 | java-version: "12.x" 24 | - uses: subosito/flutter-action@v1 25 | with: 26 | channel: "stable" 27 | - run: npm install -g firebase-tools 28 | - run: flutter pub get 29 | - name: Decode confidential files 30 | env: 31 | FIREBASE_API_KEY_IOS: ${{secrets.FIREBASE_API_KEY_IOS_DEV}} 32 | run: mkdir -p ios/Config/dev && echo $FIREBASE_API_KEY_IOS | base64 -d > ios/Config/dev/GoogleService-Info.plist 33 | # Build and publish iOS 34 | - name: Build iOS 35 | run: flutter build ios --flavor dev --dart-define APP_ENV=dev --no-codesign 36 | - name: Archive iOS 37 | working-directory: ./ios 38 | env: 39 | GIT_URL: https://github.com/tomwyr/pinger-ios-signing.git 40 | KEYCHAIN_NAME: pinger-keychain 41 | KEYCHAIN_PASSWORD: ${{secrets.KEYCHAIN_PASSWORD}} 42 | MATCH_PASSWORD: ${{secrets.MATCH_PASSWORD}} 43 | GIT_BASIC_AUTHORIZATION: ${{secrets.GIT_BASIC_AUTHORIZATION}} 44 | IOS_SIGNING_IDENTITY: ${{secrets.IOS_SIGNING_IDENTITY}} 45 | PROFILE_TYPE: adhoc 46 | EXPORT_METHOD: ad-hoc 47 | run: fastlane sign_and_build_app 48 | - name: Publish iOS 49 | working-directory: ./ios 50 | env: 51 | FIREBASE_APP_ID: 1:19662603391:ios:ebe2f5ff1246910bbd2b4a 52 | run: fastlane publish_firebase 53 | -------------------------------------------------------------------------------- /mobile/.github/workflows/release_android.yml: -------------------------------------------------------------------------------- 1 | name: release_android 2 | 3 | on: 4 | push: 5 | tags: v* 6 | 7 | jobs: 8 | build_and_publish_android_to_store: 9 | name: Publish Android app for release 10 | runs-on: macos-latest 11 | env: 12 | IS_RELEASE: true 13 | APP_ID: com.tomwyr.pinger 14 | APP_FLAVOR: prod 15 | steps: 16 | # Setup environment 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-java@v1 19 | with: 20 | java-version: "12.x" 21 | - uses: subosito/flutter-action@v1 22 | with: 23 | channel: "stable" 24 | - run: flutter pub get 25 | - name: Decode confidential files 26 | env: 27 | SIGNING_KEY: ${{secrets.ANDROID_SIGNING_KEY}} 28 | FIREBASE_API_KEY_ANDROID: ${{secrets.FIREBASE_API_KEY_ANDROID_PROD}} 29 | run: echo $SIGNING_KEY | base64 -d > android/app/key.jks && echo $FIREBASE_API_KEY_ANDROID | base64 -d > android/app/src/prod/google-services.json 30 | # Build and publish Android 31 | - name: Build Android 32 | env: 33 | SIGNING_PASSWORD: ${{secrets.ANDROID_SIGNING_PASSWORD}} 34 | KEY_PASSWORD: ${{secrets.ANDROID_KEY_PASSWORD}} 35 | run: flutter build appbundle --flavor prod --dart-define APP_ENV=prod 36 | - name: Publish Android 37 | working-directory: ./android 38 | env: 39 | JSON_KEY_DATA: ${{secrets.GOOGLE_CLOUD_SERVICE_ACCOUNT_KEY}} 40 | run: fastlane publish_play_store 41 | -------------------------------------------------------------------------------- /mobile/.github/workflows/release_ios.yml: -------------------------------------------------------------------------------- 1 | name: release_ios 2 | 3 | on: 4 | push: 5 | tags: v* 6 | 7 | jobs: 8 | build_and_publish_ios_to_store: 9 | if: false 10 | name: Publish iOS app for release 11 | runs-on: macos-latest 12 | env: 13 | IS_RELEASE: true 14 | APP_ID: com.tomwyr.pinger 15 | APP_FLAVOR: prod 16 | steps: 17 | # Setup environment 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-java@v1 20 | with: 21 | java-version: "12.x" 22 | - uses: subosito/flutter-action@v1 23 | with: 24 | channel: "stable" 25 | - run: flutter pub get 26 | - run: bundle update --bundler 27 | working-directory: ./ios 28 | - name: Decode confidential files 29 | env: 30 | FIREBASE_API_KEY_IOS: ${{secrets.FIREBASE_API_KEY_IOS_PROD}} 31 | run: mkdir -p ios/Config/prod && echo $FIREBASE_API_KEY_IOS | base64 -d > ios/Config/prod/GoogleService-Info.plist 32 | # Build and publish iOS 33 | - name: Build iOS 34 | run: flutter build ios --flavor prod --dart-define APP_ENV=prod --no-codesign 35 | - name: Archive iOS 36 | working-directory: ./ios 37 | env: 38 | GIT_URL: https://github.com/tomwyr/pinger-ios-signing.git 39 | KEYCHAIN_NAME: pinger-keychain 40 | KEYCHAIN_PASSWORD: ${{secrets.KEYCHAIN_PASSWORD}} 41 | MATCH_PASSWORD: ${{secrets.MATCH_PASSWORD}} 42 | GIT_BASIC_AUTHORIZATION: ${{secrets.GIT_BASIC_AUTHORIZATION}} 43 | IOS_SIGNING_IDENTITY: ${{secrets.IOS_SIGNING_IDENTITY}} 44 | PROFILE_TYPE: appstore 45 | EXPORT_METHOD: app-store 46 | run: bundle exec fastlane sign_and_build_app 47 | - name: Publish iOS 48 | working-directory: ./ios 49 | env: 50 | FASTLANE_USER: ${{secrets.APPLE_ID}} 51 | FASTLANE_PASSWORD: ${{secrets.APPLE_ID_PASSWORD}} 52 | FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{secrets.APPLE_APPLICATION_SPECIFIC_PASSWORD}} 53 | FASTLANE_SESSION: ${{secrets.SPACEAUTH_SESSION}} 54 | run: bundle exec fastlane publish_app_store 55 | -------------------------------------------------------------------------------- /mobile/.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: run_tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | run_code_tests: 11 | name: Run unit and widget tests 12 | runs-on: macos-latest 13 | steps: 14 | # Setup environment 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-java@v1 17 | with: 18 | java-version: "12.x" 19 | - uses: subosito/flutter-action@v1 20 | with: 21 | channel: "stable" 22 | - run: flutter pub get 23 | # Run tests 24 | - run: flutter test 25 | -------------------------------------------------------------------------------- /mobile/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | 39 | # Google API keys 40 | ios/Config/**/GoogleService-Info.plist 41 | android/app/src/**/google-services.json 42 | -------------------------------------------------------------------------------- /mobile/.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: "2524052335ec76bb03e04ede244b071f1b86d190" 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: 2524052335ec76bb03e04ede244b071f1b86d190 17 | base_revision: 2524052335ec76bb03e04ede244b071f1b86d190 18 | - platform: android 19 | create_revision: 2524052335ec76bb03e04ede244b071f1b86d190 20 | base_revision: 2524052335ec76bb03e04ede244b071f1b86d190 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /mobile/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "dev", 9 | "request": "launch", 10 | "type": "dart", 11 | "args": ["--flavor", "dev", "--dart-define", "APP_ENV=dev"] 12 | }, 13 | { 14 | "name": "prod", 15 | "request": "launch", 16 | "type": "dart", 17 | "args": ["--flavor", "prod", "--dart-define", "APP_ENV=prod"] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /mobile/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.lineLength": 100, 3 | "[dart]": { 4 | "editor.rulers": [ 5 | 100 6 | ] 7 | } 8 | } -------------------------------------------------------------------------------- /mobile/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tomasz Wyrowiński 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /mobile/README.md: -------------------------------------------------------------------------------- 1 | # Pinger 2 | 3 | Ping command utility app made with Flutter. 4 | 5 | [Download on the App Store](https://apps.apple.com/app/id1520063947) 6 | [Get it on Google Play](https://play.google.com/store/apps/details?id=com.tomwyr.pinger) 7 | 8 | 9 | 10 | ### Used libraries, tools and services: 11 | - (some) community packages: 12 | - [mobx](https://pub.dev/packages/mobx) 13 | - [freezed](https://pub.dev/packages/freezed) 14 | - [injectable](https://pub.dev/packages/injectable) 15 | - [fl_chart](https://pub.dev/packages/fl_chart) 16 | - [Firebase](https://github.com/FirebaseExtended/flutterfire) 17 | - Firestore 18 | - App Distribution 19 | - Crashlytics 20 | - [Fastlane](https://docs.fastlane.tools/) 21 | - [GitHub Actions](https://help.github.com/en/actions) 22 | -------------------------------------------------------------------------------- /mobile/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: [lib/**.*.dart] 5 | 6 | linter: 7 | rules: 8 | sort_constructors_first: true 9 | avoid_function_literals_in_foreach_calls: false 10 | -------------------------------------------------------------------------------- /mobile/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/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /mobile/android/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | 5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 6 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 7 | -------------------------------------------------------------------------------- /mobile/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | def keystoreProperties = new Properties() 26 | if (System.env.IS_RELEASE) { 27 | keystoreProperties.setProperty('storePassword', System.getenv('SIGNING_PASSWORD')) 28 | keystoreProperties.setProperty('keyPassword', System.getenv('KEY_PASSWORD')) 29 | keystoreProperties.setProperty('keyAlias', 'upload') 30 | keystoreProperties.setProperty('storeFile', 'key.jks') 31 | } 32 | 33 | android { 34 | namespace "com.tomwyr.pinger" 35 | ndkVersion flutter.ndkVersion 36 | 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_1_8 39 | targetCompatibility JavaVersion.VERSION_1_8 40 | } 41 | 42 | kotlinOptions { 43 | jvmTarget = '1.8' 44 | } 45 | 46 | sourceSets { 47 | main.java.srcDirs += 'src/main/kotlin' 48 | } 49 | 50 | defaultConfig { 51 | applicationId "com.tomwyr.pinger" 52 | // You can update the following values to match your application needs. 53 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 54 | minSdkVersion flutter.minSdkVersion 55 | targetSdkVersion flutter.targetSdkVersion 56 | compileSdk flutter.compileSdkVersion 57 | versionCode flutterVersionCode.toInteger() 58 | versionName flutterVersionName 59 | } 60 | 61 | signingConfigs { 62 | release { 63 | keyAlias keystoreProperties['keyAlias'] 64 | keyPassword keystoreProperties['keyPassword'] 65 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 66 | storePassword keystoreProperties['storePassword'] 67 | } 68 | } 69 | 70 | flavorDimensions = ["app"] 71 | productFlavors { 72 | dev { 73 | dimension "app" 74 | applicationIdSuffix ".dev" 75 | resValue "string", "app_name", "[dev] Pinger" 76 | } 77 | prod { 78 | dimension "app" 79 | resValue "string", "app_name", "Pinger" 80 | signingConfig signingConfigs.release 81 | } 82 | } 83 | 84 | buildTypes { 85 | release { 86 | signingConfig signingConfigs.release 87 | } 88 | } 89 | } 90 | 91 | flutter { 92 | source '../..' 93 | } 94 | 95 | dependencies {} 96 | -------------------------------------------------------------------------------- /mobile/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 11 | 19 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 39 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java: -------------------------------------------------------------------------------- 1 | // Generated file. 2 | // 3 | // If you wish to remove Flutter's multidex support, delete this entire file. 4 | // 5 | // Modifications to this file should be done in a copy under a different name 6 | // as this file may be regenerated. 7 | 8 | package io.flutter.app; 9 | 10 | import android.app.Application; 11 | import android.content.Context; 12 | import androidx.annotation.CallSuper; 13 | import androidx.multidex.MultiDex; 14 | 15 | /** 16 | * Extension of {@link android.app.Application}, adding multidex support. 17 | */ 18 | public class FlutterMultiDexApplication extends Application { 19 | @Override 20 | @CallSuper 21 | protected void attachBaseContext(Context base) { 22 | super.attachBaseContext(base); 23 | MultiDex.install(this); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/kotlin/com/tomwyr/pinger/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomwyr.pinger 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/main/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/main/res/drawable/ic_notification.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F3D56 4 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /mobile/android/app/src/prod/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/prod/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/prod/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/prod/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/prod/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/prod/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mobile/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.9.0' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.4.2' 10 | // START: FlutterFire Configuration 11 | classpath 'com.google.gms:google-services:4.3.10' 12 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' 13 | // END: FlutterFire Configuration 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | } 23 | } 24 | 25 | rootProject.buildDir = '../build' 26 | subprojects { 27 | project.buildDir = "${rootProject.buildDir}/${project.name}" 28 | } 29 | subprojects { 30 | project.evaluationDependsOn(':app') 31 | } 32 | 33 | tasks.register("clean", Delete) { 34 | delete rootProject.buildDir 35 | } 36 | -------------------------------------------------------------------------------- /mobile/android/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | package_name("com.tomwyr.pinger") 2 | -------------------------------------------------------------------------------- /mobile/android/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:android) 2 | 3 | flavor = ENV["APP_FLAVOR"] 4 | 5 | platform :android do 6 | lane :publish_firebase do 7 | firebase_app_distribution( 8 | app: ENV["FIREBASE_APP_ID"], 9 | groups: ENV["FIREBASE_GROUPS"], 10 | firebase_cli_token: ENV["FIREBASE_TOKEN"], 11 | apk_path: "../build/app/outputs/apk/#{flavor}/release/app-#{flavor}-release.apk", 12 | ) 13 | end 14 | 15 | lane :publish_play_store do 16 | upload_to_play_store( 17 | json_key_data: ENV["JSON_KEY_DATA"], 18 | aab: "../build/app/outputs/bundle/#{flavor}Release/app-#{flavor}-release.aab", 19 | ) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /mobile/android/fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-firebase_app_distribution' 6 | -------------------------------------------------------------------------------- /mobile/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /mobile/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-7.5-all.zip 6 | -------------------------------------------------------------------------------- /mobile/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 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | plugins { 14 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 15 | } 16 | } 17 | 18 | include ":app" 19 | 20 | apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" 21 | -------------------------------------------------------------------------------- /mobile/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | json_serializable: 5 | options: 6 | explicit_to_json: true 7 | -------------------------------------------------------------------------------- /mobile/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /mobile/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /mobile/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /mobile/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /mobile/ios/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # https://github.com/fastlane/fastlane/issues/16621 4 | gem "fastlane", ">= 2.150.0.rc3" 5 | 6 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 7 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 8 | -------------------------------------------------------------------------------- /mobile/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '10.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/dev.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/prod.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /mobile/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /mobile/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | SimplePingChannel().register(controller: window.rootViewController as! FlutterViewController) 12 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-1024x1024@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-20x20@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-20x20@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-20x20@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-29x29@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-29x29@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-29x29@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-40x40@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-40x40@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-40x40@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-60x60@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-60x60@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-76x76@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-76x76@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/AppIcon-dev-83.5x83.5@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-dev-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-dev-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-dev-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-dev-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-dev-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-dev-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-dev-40x40@3x.png","scale":"3x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-dev-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-dev-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-dev-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-dev-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-dev-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-dev-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-dev-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-dev-40x40@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-dev-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-dev-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-dev-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-dev-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-1024x1024@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-20x20@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-20x20@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-20x20@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-29x29@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-29x29@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-29x29@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-40x40@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-40x40@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-40x40@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-60x60@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-60x60@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-76x76@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-76x76@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/AppIcon-prod-83.5x83.5@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-prod-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-prod-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-prod-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-prod-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-prod-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-prod-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-prod-40x40@3x.png","scale":"3x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-prod-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-prod-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-prod-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-prod-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-prod-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-prod-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-prod-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-prod-40x40@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-prod-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-prod-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-prod-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-prod-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /mobile/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /mobile/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleLocalizations 6 | 7 | en 8 | fr 9 | pl 10 | 11 | CFBundleDevelopmentRegion 12 | $(DEVELOPMENT_LANGUAGE) 13 | CFBundleExecutable 14 | $(EXECUTABLE_NAME) 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | $(APP_DISPLAY_NAME) 21 | CFBundleDisplayName 22 | $(APP_DISPLAY_NAME) 23 | CFBundlePackageType 24 | APPL 25 | CFBundleShortVersionString 26 | $(MARKETING_VERSION) 27 | CFBundleSignature 28 | ???? 29 | CFBundleVersion 30 | $(CURRENT_PROJECT_VERSION) 31 | LSRequiresIPhoneOS 32 | 33 | NSLocationWhenInUseUsageDescription 34 | Your location will be used to allow other users to compare their results with results in your world region. 35 | NSLocationAlwaysUsageDescription 36 | Your location will be used to allow other users to compare their results with results in your world region. 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIMainStoryboardFile 40 | Main 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /mobile/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | #import "SimplePing.h" 3 | -------------------------------------------------------------------------------- /mobile/ios/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:ios) 2 | 3 | platform :ios do 4 | lane :sign_and_build_app do 5 | appId = ENV["APP_ID"] 6 | profileType = ENV["PROFILE_TYPE"] 7 | 8 | create_keychain( 9 | name: ENV["KEYCHAIN_NAME"], 10 | password: ENV["KEYCHAIN_PASSWORD"], 11 | timeout: false, 12 | unlock: true, 13 | ) 14 | 15 | match( 16 | git_url: ENV["GIT_URL"], 17 | git_basic_authorization: ENV["GIT_BASIC_AUTHORIZATION"], 18 | keychain_name: ENV["KEYCHAIN_NAME"], 19 | keychain_password: ENV["KEYCHAIN_PASSWORD"], 20 | type: profileType, 21 | app_identifier: appId, 22 | readonly: true, 23 | ) 24 | 25 | update_project_provisioning( 26 | xcodeproj: "Runner.xcodeproj", 27 | target_filter: "Runner", 28 | build_configuration: "Release", 29 | profile: ENV["sigh_#{appId}_#{profileType}_profile-path"], 30 | code_signing_identity: ENV["IOS_SIGNING_IDENTITY"], 31 | ) 32 | 33 | update_code_signing_settings( 34 | use_automatic_signing: false, 35 | path: "Runner.xcodeproj", 36 | ) 37 | 38 | build_app( 39 | workspace: "Runner.xcworkspace", 40 | scheme: ENV["APP_FLAVOR"], 41 | export_method: ENV["EXPORT_METHOD"], 42 | output_name: "Runner.ipa", 43 | export_options: { 44 | provisioningProfiles: { 45 | appId => ENV["sigh_#{appId}_#{profileType}_profile-name"] 46 | } 47 | }, 48 | ) 49 | end 50 | 51 | lane :publish_firebase do 52 | firebase_app_distribution( 53 | app: ENV["FIREBASE_APP_ID"], 54 | groups: ENV["FIREBASE_GROUPS"], 55 | firebase_cli_token: ENV["FIREBASE_TOKEN"], 56 | ) 57 | end 58 | 59 | lane :publish_app_store do 60 | upload_to_app_store( 61 | force: true, 62 | skip_metadata: true, 63 | skip_screenshots: true, 64 | metadata_path: nil, 65 | ) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /mobile/ios/fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-firebase_app_distribution' 6 | -------------------------------------------------------------------------------- /mobile/ios/firebase_app_id_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_generated_by": "FlutterFire CLI", 3 | "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", 4 | "GOOGLE_APP_ID": "1:35379423863:ios:9c2e269734499a359c7472", 5 | "FIREBASE_PROJECT_ID": "pinger-855aa", 6 | "GCM_SENDER_ID": "35379423863" 7 | } -------------------------------------------------------------------------------- /mobile/lib/assets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class Images { 4 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/icons/app-icon-dev.png) 5 | static AssetImage get appIconDev => const AssetImage("lib/assets/icons/app-icon-dev.png"); 6 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/icons/app-icon-prod.png) 7 | static AssetImage get appIconProd => const AssetImage("lib/assets/icons/app-icon-prod.png"); 8 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/splash-hd.png) 9 | static AssetImage get splashHd => const AssetImage("lib/assets/images/splash-hd.png"); 10 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/splash.png) 11 | static AssetImage get splash => const AssetImage("lib/assets/images/splash.png"); 12 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/undraw_collecting.png) 13 | static AssetImage get undrawCollecting => const AssetImage("lib/assets/images/undraw_collecting.png"); 14 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/undraw_empty.png) 15 | static AssetImage get undrawEmpty => const AssetImage("lib/assets/images/undraw_empty.png"); 16 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/undraw_road_sign.png) 17 | static AssetImage get undrawRoadSign => const AssetImage("lib/assets/images/undraw_road_sign.png"); 18 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/undraw_runner_start.png) 19 | static AssetImage get undrawRunnerStart => const AssetImage("lib/assets/images/undraw_runner_start.png"); 20 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/undraw_searching.png) 21 | static AssetImage get undrawSearching => const AssetImage("lib/assets/images/undraw_searching.png"); 22 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/undraw_server_down.png) 23 | static AssetImage get undrawServerDown => const AssetImage("lib/assets/images/undraw_server_down.png"); 24 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/undraw_settings.png) 25 | static AssetImage get undrawSettings => const AssetImage("lib/assets/images/undraw_settings.png"); 26 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/undraw_signal_searching.png) 27 | static AssetImage get undrawSignalSearching => const AssetImage("lib/assets/images/undraw_signal_searching.png"); 28 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/undraw_the_world_is_mine.png) 29 | static AssetImage get undrawTheWorldIsMine => const AssetImage("lib/assets/images/undraw_the_world_is_mine.png"); 30 | /// ![](file:///C:/Users/tomwyr/dev/projects/pinger/lib/assets/images/undraw_void.png) 31 | static AssetImage get undrawVoid => const AssetImage("lib/assets/images/undraw_void.png"); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /mobile/lib/assets/icons/app-icon-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/icons/app-icon-dev.png -------------------------------------------------------------------------------- /mobile/lib/assets/icons/app-icon-prod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/icons/app-icon-prod.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/splash-hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/splash-hd.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/splash.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/undraw_collecting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/undraw_collecting.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/undraw_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/undraw_empty.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/undraw_road_sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/undraw_road_sign.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/undraw_runner_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/undraw_runner_start.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/undraw_searching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/undraw_searching.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/undraw_server_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/undraw_server_down.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/undraw_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/undraw_settings.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/undraw_signal_searching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/undraw_signal_searching.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/undraw_the_world_is_mine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/undraw_the_world_is_mine.png -------------------------------------------------------------------------------- /mobile/lib/assets/images/undraw_void.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomwyr/pinger/af454877f0603f4620e21fcb5f3e2d4a3d1aec37/mobile/lib/assets/images/undraw_void.png -------------------------------------------------------------------------------- /mobile/lib/config.dart: -------------------------------------------------------------------------------- 1 | import 'package:injectable/injectable.dart'; 2 | 3 | import 'package:pinger/assets.dart'; 4 | 5 | abstract class AppConfig { 6 | String get iconPath; 7 | } 8 | 9 | @dev 10 | @Injectable(as: AppConfig) 11 | class DevConfig extends AppConfig { 12 | @override 13 | String get iconPath => Images.appIconDev.assetName; 14 | } 15 | 16 | @prod 17 | @Injectable(as: AppConfig) 18 | class ProdConfig extends AppConfig { 19 | @override 20 | String get iconPath => Images.appIconProd.assetName; 21 | } 22 | -------------------------------------------------------------------------------- /mobile/lib/di/injector.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:connectivity/connectivity.dart'; 3 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 4 | import 'package:get_it/get_it.dart'; 5 | import 'package:injectable/injectable.dart'; 6 | import 'package:location/location.dart'; 7 | import 'package:package_info/package_info.dart'; 8 | import 'package:pinger/di/injector.config.dart'; 9 | import 'package:pinger/store/device_store.dart'; 10 | import 'package:pinger/store/hosts_store.dart'; 11 | import 'package:pinger/store/permission_store.dart'; 12 | import 'package:pinger/store/ping_store.dart'; 13 | import 'package:pinger/store/results_store.dart'; 14 | import 'package:pinger/store/settings_store.dart'; 15 | import 'package:pinger/utils/notification_messages.dart'; 16 | import 'package:shared_preferences/shared_preferences.dart'; 17 | 18 | abstract class Injector { 19 | static Future configure(String environment) async { 20 | await _initInjectable(environment); 21 | resolve().init(); 22 | resolve().init(); 23 | resolve().init(); 24 | resolve().init(); 25 | await resolve(PermissionStore.location).init(); 26 | await resolve(PermissionStore.notification).init(); 27 | resolve().init(); 28 | } 29 | 30 | static T resolve([String? instanceName]) => 31 | GetIt.instance.get(instanceName: instanceName); 32 | } 33 | 34 | @module 35 | abstract class InjectorModule { 36 | @preResolve 37 | Future get sharedPreferences => SharedPreferences.getInstance(); 38 | 39 | @preResolve 40 | Future get packageInfo => PackageInfo.fromPlatform(); 41 | 42 | Location get location => Location.instance; 43 | 44 | FirebaseFirestore get firestore => FirebaseFirestore.instance; 45 | 46 | FlutterLocalNotificationsPlugin get localNotifications => FlutterLocalNotificationsPlugin() 47 | ..initialize(const InitializationSettings( 48 | android: AndroidInitializationSettings('ic_notification'), 49 | iOS: DarwinInitializationSettings( 50 | requestAlertPermission: false, 51 | requestBadgePermission: false, 52 | requestSoundPermission: false, 53 | ), 54 | )); 55 | 56 | NotificationMessages get notificationLocalizations => NotificationMessages(); 57 | 58 | Connectivity get connectivity => Connectivity(); 59 | } 60 | 61 | @injectableInit 62 | Future _initInjectable(String environment) => GetIt.instance.init(environment: environment); 63 | -------------------------------------------------------------------------------- /mobile/lib/extensions.dart: -------------------------------------------------------------------------------- 1 | extension IterableExtensions on Iterable { 2 | Iterable mapIndexed(T Function(int i, E e) f) { 3 | int index = -1; 4 | return map((e) => f(++index, e)); 5 | } 6 | 7 | E? get lastOrNull => isNotEmpty ? last : null; 8 | } 9 | 10 | extension NullableTypeIterableExtensions on Iterable { 11 | Iterable whereNotNull() sync* { 12 | for (final e in this) { 13 | if (e != null) yield e; 14 | } 15 | } 16 | } 17 | 18 | extension NullableIterableExtensions on Iterable? { 19 | bool get isNullOrEmpty => this?.isEmpty ?? true; 20 | } 21 | -------------------------------------------------------------------------------- /mobile/lib/firebase_options.dart: -------------------------------------------------------------------------------- 1 | // File generated by FlutterFire CLI. 2 | // ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members 3 | import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 | import 'package:flutter/foundation.dart' 5 | show defaultTargetPlatform, kIsWeb, TargetPlatform; 6 | 7 | /// Default [FirebaseOptions] for use with your Firebase apps. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// import 'firebase_options.dart'; 12 | /// // ... 13 | /// await Firebase.initializeApp( 14 | /// options: DefaultFirebaseOptions.currentPlatform, 15 | /// ); 16 | /// ``` 17 | class DefaultFirebaseOptions { 18 | static FirebaseOptions get currentPlatform { 19 | if (kIsWeb) { 20 | throw UnsupportedError( 21 | 'DefaultFirebaseOptions have not been configured for web - ' 22 | 'you can reconfigure this by running the FlutterFire CLI again.', 23 | ); 24 | } 25 | switch (defaultTargetPlatform) { 26 | case TargetPlatform.android: 27 | return android; 28 | case TargetPlatform.iOS: 29 | return ios; 30 | case TargetPlatform.macOS: 31 | throw UnsupportedError( 32 | 'DefaultFirebaseOptions have not been configured for macos - ' 33 | 'you can reconfigure this by running the FlutterFire CLI again.', 34 | ); 35 | case TargetPlatform.windows: 36 | throw UnsupportedError( 37 | 'DefaultFirebaseOptions have not been configured for windows - ' 38 | 'you can reconfigure this by running the FlutterFire CLI again.', 39 | ); 40 | case TargetPlatform.linux: 41 | throw UnsupportedError( 42 | 'DefaultFirebaseOptions have not been configured for linux - ' 43 | 'you can reconfigure this by running the FlutterFire CLI again.', 44 | ); 45 | default: 46 | throw UnsupportedError( 47 | 'DefaultFirebaseOptions are not supported for this platform.', 48 | ); 49 | } 50 | } 51 | 52 | static const FirebaseOptions android = FirebaseOptions( 53 | apiKey: 'AIzaSyBSrzk81fDemMcHFDszV0IahN8eEUkPKjA', 54 | appId: '1:35379423863:android:074b3ae36c762aa59c7472', 55 | messagingSenderId: '35379423863', 56 | projectId: 'pinger-855aa', 57 | databaseURL: 'https://pinger-855aa.firebaseio.com', 58 | storageBucket: 'pinger-855aa.appspot.com', 59 | ); 60 | 61 | static const FirebaseOptions ios = FirebaseOptions( 62 | apiKey: 'AIzaSyBgf_Q4lVy6NN2GwsPDa7AzD9olnt_U32Y', 63 | appId: '1:35379423863:ios:9c2e269734499a359c7472', 64 | messagingSenderId: '35379423863', 65 | projectId: 'pinger-855aa', 66 | databaseURL: 'https://pinger-855aa.firebaseio.com', 67 | storageBucket: 'pinger-855aa.appspot.com', 68 | iosClientId: '35379423863-eqdpjv2jn1lkmak8u71hv4mm4il2ga6j.apps.googleusercontent.com', 69 | iosBundleId: 'com.tomwyr.pinger', 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /mobile/lib/generated/intl/messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names, unnecessary_new 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'dart:async'; 13 | 14 | import 'package:flutter/foundation.dart'; 15 | import 'package:intl/intl.dart'; 16 | import 'package:intl/message_lookup_by_library.dart'; 17 | import 'package:intl/src/intl_helpers.dart'; 18 | 19 | import 'messages_en.dart' as messages_en; 20 | import 'messages_fr.dart' as messages_fr; 21 | import 'messages_pl.dart' as messages_pl; 22 | 23 | typedef Future LibraryLoader(); 24 | Map _deferredLibraries = { 25 | 'en': () => new SynchronousFuture(null), 26 | 'fr': () => new SynchronousFuture(null), 27 | 'pl': () => new SynchronousFuture(null), 28 | }; 29 | 30 | MessageLookupByLibrary? _findExact(String localeName) { 31 | switch (localeName) { 32 | case 'en': 33 | return messages_en.messages; 34 | case 'fr': 35 | return messages_fr.messages; 36 | case 'pl': 37 | return messages_pl.messages; 38 | default: 39 | return null; 40 | } 41 | } 42 | 43 | /// User programs should call this before using [localeName] for messages. 44 | Future initializeMessages(String localeName) { 45 | var availableLocale = Intl.verifiedLocale( 46 | localeName, (locale) => _deferredLibraries[locale] != null, 47 | onFailure: (_) => null); 48 | if (availableLocale == null) { 49 | return new SynchronousFuture(false); 50 | } 51 | var lib = _deferredLibraries[availableLocale]; 52 | lib == null ? new SynchronousFuture(false) : lib(); 53 | initializeInternalMessageLookup(() => new CompositeMessageLookup()); 54 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 55 | return new SynchronousFuture(true); 56 | } 57 | 58 | bool _messagesExistFor(String locale) { 59 | try { 60 | return _findExact(locale) != null; 61 | } catch (e) { 62 | return false; 63 | } 64 | } 65 | 66 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 67 | var actualLocale = 68 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); 69 | if (actualLocale == null) return null; 70 | return _findExact(actualLocale); 71 | } 72 | -------------------------------------------------------------------------------- /mobile/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:pinger/di/injector.dart'; 6 | import 'package:pinger/firebase_options.dart'; 7 | import 'package:pinger/ui/app/pinger_app.dart'; 8 | 9 | const appEnvironment = String.fromEnvironment("APP_ENV"); 10 | 11 | void main() async { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | await Firebase.initializeApp( 14 | options: DefaultFirebaseOptions.currentPlatform, 15 | ); 16 | FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError; 17 | await SystemChrome.setPreferredOrientations([ 18 | DeviceOrientation.portraitUp, 19 | DeviceOrientation.portraitDown, 20 | ]); 21 | await Injector.configure(appEnvironment); 22 | runApp(const PingerApp()); 23 | } 24 | -------------------------------------------------------------------------------- /mobile/lib/model/geo_position.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'geo_position.freezed.dart'; 4 | part 'geo_position.g.dart'; 5 | 6 | @freezed 7 | class GeoPosition with _$GeoPosition { 8 | factory GeoPosition({ 9 | required double lat, 10 | required double lon, 11 | }) = _GeoPosition; 12 | 13 | factory GeoPosition.fromJson(Map json) => _$GeoPositionFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /mobile/lib/model/geo_position.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'geo_position.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$GeoPositionImpl _$$GeoPositionImplFromJson(Map json) => 10 | _$GeoPositionImpl( 11 | lat: (json['lat'] as num).toDouble(), 12 | lon: (json['lon'] as num).toDouble(), 13 | ); 14 | 15 | Map _$$GeoPositionImplToJson(_$GeoPositionImpl instance) => 16 | { 17 | 'lat': instance.lat, 18 | 'lon': instance.lon, 19 | }; 20 | -------------------------------------------------------------------------------- /mobile/lib/model/host_stats.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'host_stats.freezed.dart'; 4 | part 'host_stats.g.dart'; 5 | 6 | @freezed 7 | class HostStats with _$HostStats { 8 | factory HostStats({ 9 | required String host, 10 | required int pingCount, 11 | required DateTime pingTime, 12 | }) = _HostStats; 13 | 14 | factory HostStats.fromJson(Map json) => _$HostStatsFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /mobile/lib/model/host_stats.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'host_stats.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$HostStatsImpl _$$HostStatsImplFromJson(Map json) => 10 | _$HostStatsImpl( 11 | host: json['host'] as String, 12 | pingCount: json['pingCount'] as int, 13 | pingTime: DateTime.parse(json['pingTime'] as String), 14 | ); 15 | 16 | Map _$$HostStatsImplToJson(_$HostStatsImpl instance) => 17 | { 18 | 'host': instance.host, 19 | 'pingCount': instance.pingCount, 20 | 'pingTime': instance.pingTime.toIso8601String(), 21 | }; 22 | -------------------------------------------------------------------------------- /mobile/lib/model/ping_global.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'package:pinger/model/geo_position.dart'; 4 | import 'package:pinger/model/ping_result.dart'; 5 | 6 | part 'ping_global.freezed.dart'; 7 | part 'ping_global.g.dart'; 8 | 9 | @freezed 10 | class GlobalPingCounts with _$GlobalPingCounts { 11 | factory GlobalPingCounts({ 12 | required int totalCount, 13 | required List pingCounts, 14 | }) = _GlobalPingCounts; 15 | 16 | factory GlobalPingCounts.fromJson(Map json) => _$GlobalPingCountsFromJson(json); 17 | 18 | factory GlobalPingCounts.empty() => GlobalPingCounts(totalCount: 0, pingCounts: []); 19 | } 20 | 21 | @freezed 22 | class PingCount with _$PingCount { 23 | factory PingCount({ 24 | required String host, 25 | required int count, 26 | }) = _PingCount; 27 | 28 | factory PingCount.fromJson(Map json) => _$PingCountFromJson(json); 29 | } 30 | 31 | @freezed 32 | class GlobalHostResults with _$GlobalHostResults { 33 | factory GlobalHostResults({ 34 | required int totalCount, 35 | required ValueResults valueResults, 36 | required List locationResults, 37 | }) = _GlobalHostResults; 38 | 39 | factory GlobalHostResults.fromJson(Map json) => 40 | _$GlobalHostResultsFromJson(json); 41 | 42 | factory GlobalHostResults.empty() => GlobalHostResults( 43 | totalCount: 0, 44 | valueResults: ValueResults(min: {}, mean: {}, max: {}), 45 | locationResults: [], 46 | ); 47 | } 48 | 49 | @freezed 50 | class ValueResults with _$ValueResults { 51 | factory ValueResults({ 52 | required Map min, 53 | required Map mean, 54 | required Map max, 55 | }) = _ValueResults; 56 | 57 | factory ValueResults.fromJson(Map json) => _$ValueResultsFromJson(json); 58 | } 59 | 60 | @freezed 61 | class LocationResults with _$LocationResults { 62 | factory LocationResults({ 63 | required int count, 64 | required GeoPosition location, 65 | required PingStats stats, 66 | }) = _LocationResults; 67 | 68 | factory LocationResults.fromJson(Map json) => _$LocationResultsFromJson(json); 69 | } 70 | 71 | @freezed 72 | class GlobalSessionResult with _$GlobalSessionResult { 73 | factory GlobalSessionResult({ 74 | required int count, 75 | required String host, 76 | required PingStats stats, 77 | GeoPosition? location, 78 | }) = _GlobalSessionResult; 79 | 80 | factory GlobalSessionResult.fromJson(Map json) => 81 | _$GlobalSessionResultFromJson(json); 82 | } 83 | -------------------------------------------------------------------------------- /mobile/lib/model/ping_result.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | 5 | import 'package:pinger/model/user_settings.dart'; 6 | 7 | part 'ping_result.freezed.dart'; 8 | part 'ping_result.g.dart'; 9 | 10 | @freezed 11 | class PingResult with _$PingResult { 12 | factory PingResult({ 13 | int? id, 14 | required String host, 15 | required PingSettings settings, 16 | required DateTime startTime, 17 | required Duration duration, 18 | required List values, 19 | required PingStats stats, 20 | }) = _PingResult; 21 | 22 | factory PingResult.fromJson(Map json) => _$PingResultFromJson(json); 23 | } 24 | 25 | @freezed 26 | class PingStats with _$PingStats { 27 | const factory PingStats({ 28 | required int min, 29 | required int max, 30 | required int mean, 31 | }) = _PingStats; 32 | 33 | factory PingStats.fromJson(Map json) => _$PingStatsFromJson(json); 34 | 35 | static PingStats? fromValues(Iterable values) { 36 | bool hasValue = false; 37 | int min = double.maxFinite.toInt(); 38 | int max = 0; 39 | int sum = 0; 40 | values.where((it) => it != null).forEach((it) { 41 | if (!hasValue) hasValue = true; 42 | sum += it!; 43 | min = math.min(min, it); 44 | max = math.max(max, it); 45 | }); 46 | if (!hasValue) return null; 47 | return PingStats( 48 | min: min.toInt(), 49 | max: max.toInt(), 50 | mean: sum ~/ values.length, 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /mobile/lib/model/ping_result.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'ping_result.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$PingResultImpl _$$PingResultImplFromJson(Map json) => 10 | _$PingResultImpl( 11 | id: json['id'] as int?, 12 | host: json['host'] as String, 13 | settings: PingSettings.fromJson(json['settings'] as Map), 14 | startTime: DateTime.parse(json['startTime'] as String), 15 | duration: Duration(microseconds: json['duration'] as int), 16 | values: (json['values'] as List).map((e) => e as int?).toList(), 17 | stats: PingStats.fromJson(json['stats'] as Map), 18 | ); 19 | 20 | Map _$$PingResultImplToJson(_$PingResultImpl instance) => 21 | { 22 | 'id': instance.id, 23 | 'host': instance.host, 24 | 'settings': instance.settings.toJson(), 25 | 'startTime': instance.startTime.toIso8601String(), 26 | 'duration': instance.duration.inMicroseconds, 27 | 'values': instance.values, 28 | 'stats': instance.stats.toJson(), 29 | }; 30 | 31 | _$PingStatsImpl _$$PingStatsImplFromJson(Map json) => 32 | _$PingStatsImpl( 33 | min: json['min'] as int, 34 | max: json['max'] as int, 35 | mean: json['mean'] as int, 36 | ); 37 | 38 | Map _$$PingStatsImplToJson(_$PingStatsImpl instance) => 39 | { 40 | 'min': instance.min, 41 | 'max': instance.max, 42 | 'mean': instance.mean, 43 | }; 44 | -------------------------------------------------------------------------------- /mobile/lib/model/ping_session.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'package:pinger/model/ping_result.dart'; 4 | import 'package:pinger/model/user_settings.dart'; 5 | 6 | part 'ping_session.freezed.dart'; 7 | 8 | @freezed 9 | class PingSession with _$PingSession { 10 | PingSession._(); 11 | 12 | factory PingSession({ 13 | required String host, 14 | required PingStatus status, 15 | required PingSettings settings, 16 | List? values, 17 | DateTime? startTime, 18 | }) = _PingSession; 19 | 20 | late final PingStats? stats = values != null ? PingStats.fromValues(values!) : null; 21 | } 22 | 23 | enum PingStatus { 24 | initial, 25 | quickCheckStarted, 26 | quickCheckLocked, 27 | sessionStarted, 28 | sessionPaused, 29 | sessionDone, 30 | } 31 | 32 | extension PingStatusExtensions on PingStatus? { 33 | bool get isNull => this == null; 34 | 35 | bool get isInitial => this == PingStatus.initial; 36 | bool get isQuickCheckStarted => this == PingStatus.quickCheckStarted; 37 | bool get isQuickCheckLocked => this == PingStatus.quickCheckLocked; 38 | bool get isSessionStarted => this == PingStatus.sessionStarted; 39 | bool get isSessionPaused => this == PingStatus.sessionPaused; 40 | bool get isSessionDone => this == PingStatus.sessionDone; 41 | 42 | bool get isQuickCheck => 43 | this == PingStatus.quickCheckStarted || this == PingStatus.quickCheckLocked; 44 | bool get isStarted => 45 | this == PingStatus.quickCheckStarted || 46 | this == PingStatus.quickCheckLocked || 47 | this == PingStatus.sessionStarted; 48 | bool get isSession => 49 | this == PingStatus.sessionStarted || 50 | this == PingStatus.sessionPaused || 51 | this == PingStatus.sessionDone; 52 | } 53 | -------------------------------------------------------------------------------- /mobile/lib/model/user_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'user_settings.freezed.dart'; 4 | part 'user_settings.g.dart'; 5 | 6 | @freezed 7 | class UserSettings with _$UserSettings { 8 | factory UserSettings({ 9 | required PingSettings pingSettings, 10 | required ShareSettings shareSettings, 11 | TraySettings? traySettings, 12 | required bool showSystemNotification, 13 | required bool restoreHost, 14 | required bool nightMode, 15 | }) = _UserSettings; 16 | 17 | factory UserSettings.fromJson(Map json) => _$UserSettingsFromJson(json); 18 | } 19 | 20 | @freezed 21 | class ShareSettings with _$ShareSettings { 22 | factory ShareSettings({ 23 | required bool shareResults, 24 | required bool attachLocation, 25 | }) = _ShareSettings; 26 | 27 | factory ShareSettings.fromJson(Map json) => _$ShareSettingsFromJson(json); 28 | } 29 | 30 | @freezed 31 | class PingSettings with _$PingSettings { 32 | factory PingSettings({ 33 | required NumSetting count, 34 | required int packetSize, 35 | required int interval, 36 | required int timeout, 37 | }) = _PingSettings; 38 | 39 | factory PingSettings.fromJson(Map json) => _$PingSettingsFromJson(json); 40 | } 41 | 42 | @freezed 43 | class NumSetting with _$NumSetting { 44 | const factory NumSetting.finite({required int value}) = _FiniteNumSetting; 45 | const factory NumSetting.infinite() = _InfiniteNumSetting; 46 | 47 | factory NumSetting.fromJson(json) => 48 | json is int ? NumSetting.finite(value: json) : _$NumSettingFromJson(json); 49 | } 50 | 51 | @freezed 52 | class TraySettings with _$TraySettings { 53 | factory TraySettings({ 54 | required bool enabled, 55 | required bool autoReveal, 56 | }) = _TraySettings; 57 | 58 | factory TraySettings.fromJson(Map json) => _$TraySettingsFromJson(json); 59 | } 60 | -------------------------------------------------------------------------------- /mobile/lib/service/favicon_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:http/http.dart' as http; 5 | import 'package:injectable/injectable.dart'; 6 | import 'package:path_provider/path_provider.dart' as path; 7 | 8 | @injectable 9 | class FaviconService { 10 | const FaviconService( 11 | this.baseUrl, 12 | this.validFormats, 13 | this.expiryTime, 14 | ); 15 | 16 | @factoryMethod 17 | factory FaviconService.create() => const FaviconService( 18 | googleBaseUrl, 19 | {'image/png', 'image/vnd.microsoft.icon'}, 20 | Duration(days: 7), 21 | ); 22 | 23 | static const faviconKitBaseUrl = 'https://api.faviconkit.com/'; 24 | static const googleBaseUrl = 'https://www.google.com/s2/favicons?sz=32&domain='; 25 | 26 | final String baseUrl; 27 | final Set validFormats; 28 | final Duration expiryTime; 29 | 30 | Future load(String url) async { 31 | final dir = await path.getApplicationDocumentsDirectory(); 32 | final file = File("${dir.path}/$url"); 33 | if (!await _isIconValid(file)) { 34 | await _tryFetchIcon(url).then(file.writeAsBytes); 35 | } 36 | return await file.readAsBytes(); 37 | } 38 | 39 | Future _tryFetchIcon(String url) async { 40 | try { 41 | final response = await http.get(Uri.parse("$baseUrl$url")); 42 | final contentType = response.headers['content-type']; 43 | if (response.statusCode != 200) { 44 | throw FaviconError.serverError; 45 | } else if (!validFormats.contains(contentType)) { 46 | throw FaviconError.invalidFormat; 47 | } 48 | return response.bodyBytes; 49 | } on SocketException { 50 | throw FaviconError.couldNotConnect; 51 | } 52 | } 53 | 54 | Future _isIconValid(File file) async { 55 | if (await file.exists()) { 56 | final modDate = await file.lastModified(); 57 | return DateTime.now().difference(modDate) < expiryTime; 58 | } 59 | return false; 60 | } 61 | } 62 | 63 | enum FaviconError { 64 | serverError, 65 | invalidFormat, 66 | couldNotConnect, 67 | } 68 | -------------------------------------------------------------------------------- /mobile/lib/service/notifications_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import 'package:pinger/extensions.dart'; 5 | import 'package:pinger/model/ping_session.dart'; 6 | import 'package:pinger/utils/format_utils.dart'; 7 | import 'package:pinger/utils/notification_messages.dart'; 8 | 9 | @injectable 10 | class NotificationsManager { 11 | NotificationsManager( 12 | this._localNotifications, 13 | this._messages, 14 | ); 15 | 16 | final FlutterLocalNotificationsPlugin _localNotifications; 17 | final NotificationMessages _messages; 18 | 19 | Future? _currentNotification; 20 | 21 | void show(PingSession session) { 22 | switch (session.status) { 23 | case PingStatus.sessionStarted: 24 | case PingStatus.quickCheckLocked: 25 | _showNotification( 26 | _messages.startedTitle(session.host), 27 | session.values!.isNotEmpty 28 | ? session.values!.lastOrNull != null 29 | ? _messages.startedBody(session.values!.last!) 30 | : "-" 31 | : "", 32 | ); 33 | break; 34 | case PingStatus.sessionPaused: 35 | _showNotification( 36 | _messages.pausedTitle(session.host), 37 | _messages.pausedBody( 38 | session.values!.length, 39 | FormatUtils.getCountLabel(session.settings.count), 40 | ), 41 | ); 42 | break; 43 | case PingStatus.sessionDone: 44 | _showNotification( 45 | _messages.doneTitle(session.host), 46 | session.stats != null 47 | ? _messages.doneBody( 48 | session.stats!.min, 49 | session.stats!.mean, 50 | session.stats!.max, 51 | ) 52 | : "", 53 | ); 54 | break; 55 | case PingStatus.initial: 56 | case PingStatus.quickCheckStarted: 57 | clear(); 58 | break; 59 | } 60 | } 61 | 62 | void _showNotification(String title, String body) { 63 | const details = NotificationDetails( 64 | android: AndroidNotificationDetails( 65 | 'pingerChannelId', 66 | 'pingerChannelName', 67 | channelDescription: 'Pinger notifications channel', 68 | playSound: false, 69 | enableVibration: false, 70 | ), 71 | iOS: DarwinNotificationDetails( 72 | presentAlert: false, 73 | presentSound: false, 74 | ), 75 | ); 76 | _currentNotification = _localNotifications.show(0, title, body, details); 77 | } 78 | 79 | void clear() { 80 | if (_currentNotification != null) { 81 | _localNotifications.cancel(0); 82 | _currentNotification = null; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /mobile/lib/service/pinger_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import 'package:pinger/model/ping_global.dart'; 7 | 8 | @injectable 9 | class PingerApi { 10 | PingerApi(this._firestore); 11 | 12 | final String _allPath = 'all'; 13 | final String _countsPath = 'counts-monthly'; 14 | final String _resultsPath = 'results-monthly'; 15 | final String _sessionsPath = 'sessions'; 16 | 17 | final FirebaseFirestore _firestore; 18 | 19 | Future getPingCounts() async { 20 | final countsDoc = _firestore.collection(_countsPath).doc(_allPath); 21 | final countsSnap = await _runCall(countsDoc.get); 22 | return countsSnap.data() != null 23 | ? GlobalPingCounts.fromJson(countsSnap.data()!) 24 | : GlobalPingCounts.empty(); 25 | } 26 | 27 | Future getHostResults(String host) async { 28 | final hostDoc = _firestore.collection(_resultsPath).doc(host); 29 | final resultsSnap = await _runCall(hostDoc.get); 30 | return resultsSnap.data() != null 31 | ? GlobalHostResults.fromJson(resultsSnap.data()!) 32 | : GlobalHostResults.empty(); 33 | } 34 | 35 | Future saveSessionResult(GlobalSessionResult result) async { 36 | final sessionsCol = _firestore.collection(_sessionsPath); 37 | await _runCall(() => sessionsCol.add(result.toJson())); 38 | } 39 | 40 | Future _runCall(Future Function() call) async { 41 | try { 42 | return await call(); 43 | } on PlatformException catch (e) { 44 | switch (e.message) { 45 | case "Failed to get document because the client is offline.": 46 | throw ApiError.clientOffline; 47 | case "PERMISSION_DENIED: Missing or insufficient permissions.": 48 | throw ApiError.accessDenied; 49 | } 50 | rethrow; 51 | } 52 | } 53 | } 54 | 55 | enum ApiError { 56 | clientOffline, 57 | accessDenied, 58 | } 59 | -------------------------------------------------------------------------------- /mobile/lib/service/vibration.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_vibrate/flutter_vibrate.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | @injectable 5 | class Vibration { 6 | void feedback() { 7 | Vibrate.feedback(FeedbackType.medium); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /mobile/lib/store/device_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'device_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers 10 | 11 | mixin _$DeviceStore on DeviceStoreBase, Store { 12 | late final _$isNetworkEnabledAtom = 13 | Atom(name: 'DeviceStoreBase.isNetworkEnabled', context: context); 14 | 15 | @override 16 | bool? get isNetworkEnabled { 17 | _$isNetworkEnabledAtom.reportRead(); 18 | return super.isNetworkEnabled; 19 | } 20 | 21 | @override 22 | set isNetworkEnabled(bool? value) { 23 | _$isNetworkEnabledAtom.reportWrite(value, super.isNetworkEnabled, () { 24 | super.isNetworkEnabled = value; 25 | }); 26 | } 27 | 28 | late final _$initAsyncAction = 29 | AsyncAction('DeviceStoreBase.init', context: context); 30 | 31 | @override 32 | Future init() { 33 | return _$initAsyncAction.run(() => super.init()); 34 | } 35 | 36 | late final _$getCurrentPositionAsyncAction = 37 | AsyncAction('DeviceStoreBase.getCurrentPosition', context: context); 38 | 39 | @override 40 | Future getCurrentPosition() { 41 | return _$getCurrentPositionAsyncAction 42 | .run(() => super.getCurrentPosition()); 43 | } 44 | 45 | @override 46 | String toString() { 47 | return ''' 48 | isNetworkEnabled: ${isNetworkEnabled} 49 | '''; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /mobile/lib/store/results_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:injectable/injectable.dart'; 2 | import 'package:mobx/mobx.dart'; 3 | 4 | import 'package:pinger/model/ping_global.dart'; 5 | import 'package:pinger/model/ping_result.dart'; 6 | import 'package:pinger/service/pinger_api.dart'; 7 | import 'package:pinger/service/pinger_prefs.dart'; 8 | import 'package:pinger/utils/data_snap.dart'; 9 | 10 | part 'results_store.g.dart'; 11 | 12 | @singleton 13 | class ResultsStore extends ResultsStoreBase with _$ResultsStore { 14 | ResultsStore(this._pingerPrefs, this._pingerApi); 15 | 16 | @override 17 | final PingerPrefs _pingerPrefs; 18 | 19 | @override 20 | final PingerApi _pingerApi; 21 | } 22 | 23 | abstract class ResultsStoreBase with Store { 24 | PingerPrefs get _pingerPrefs; 25 | PingerApi get _pingerApi; 26 | 27 | @observable 28 | List? localResults; 29 | 30 | @observable 31 | Map> globalResults = {}; 32 | 33 | @action 34 | void init() { 35 | _emitLocalResults(); 36 | } 37 | 38 | @action 39 | Future fetchGlobalResults(String host) async { 40 | final needsFetch = !globalResults.containsKey(host) || globalResults[host] is SnapError; 41 | if (needsFetch) { 42 | _setGlobalResults(host, const DataSnap.loading()); 43 | try { 44 | final results = await _pingerApi.getHostResults(host); 45 | _setGlobalResults(host, DataSnap.data(results)); 46 | } on ApiError { 47 | _setGlobalResults(host, const DataSnap.error()); 48 | } 49 | } 50 | } 51 | 52 | void _setGlobalResults(String host, DataSnap results) { 53 | globalResults = globalResults..[host] = results; 54 | } 55 | 56 | @action 57 | Future deleteLocalResult(int? resultId) async { 58 | await _pingerPrefs.deleteArchiveResult(resultId); 59 | _emitLocalResults(); 60 | } 61 | 62 | @action 63 | Future saveLocalResult(PingResult result) async { 64 | final resultId = await _pingerPrefs.saveArchiveResult(result); 65 | _emitLocalResults(); 66 | return resultId; 67 | } 68 | 69 | void _emitLocalResults() { 70 | localResults = _pingerPrefs.getArchiveResults() 71 | ..sort((e1, e2) => e2!.startTime.compareTo(e1!.startTime)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /mobile/lib/store/results_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'results_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers 10 | 11 | mixin _$ResultsStore on ResultsStoreBase, Store { 12 | late final _$localResultsAtom = 13 | Atom(name: 'ResultsStoreBase.localResults', context: context); 14 | 15 | @override 16 | List? get localResults { 17 | _$localResultsAtom.reportRead(); 18 | return super.localResults; 19 | } 20 | 21 | @override 22 | set localResults(List? value) { 23 | _$localResultsAtom.reportWrite(value, super.localResults, () { 24 | super.localResults = value; 25 | }); 26 | } 27 | 28 | late final _$globalResultsAtom = 29 | Atom(name: 'ResultsStoreBase.globalResults', context: context); 30 | 31 | @override 32 | Map> get globalResults { 33 | _$globalResultsAtom.reportRead(); 34 | return super.globalResults; 35 | } 36 | 37 | @override 38 | set globalResults(Map> value) { 39 | _$globalResultsAtom.reportWrite(value, super.globalResults, () { 40 | super.globalResults = value; 41 | }); 42 | } 43 | 44 | late final _$fetchGlobalResultsAsyncAction = 45 | AsyncAction('ResultsStoreBase.fetchGlobalResults', context: context); 46 | 47 | @override 48 | Future fetchGlobalResults(String host) { 49 | return _$fetchGlobalResultsAsyncAction 50 | .run(() => super.fetchGlobalResults(host)); 51 | } 52 | 53 | late final _$deleteLocalResultAsyncAction = 54 | AsyncAction('ResultsStoreBase.deleteLocalResult', context: context); 55 | 56 | @override 57 | Future deleteLocalResult(int? resultId) { 58 | return _$deleteLocalResultAsyncAction 59 | .run(() => super.deleteLocalResult(resultId)); 60 | } 61 | 62 | late final _$saveLocalResultAsyncAction = 63 | AsyncAction('ResultsStoreBase.saveLocalResult', context: context); 64 | 65 | @override 66 | Future saveLocalResult(PingResult result) { 67 | return _$saveLocalResultAsyncAction 68 | .run(() => super.saveLocalResult(result)); 69 | } 70 | 71 | late final _$ResultsStoreBaseActionController = 72 | ActionController(name: 'ResultsStoreBase', context: context); 73 | 74 | @override 75 | void init() { 76 | final _$actionInfo = _$ResultsStoreBaseActionController.startAction( 77 | name: 'ResultsStoreBase.init'); 78 | try { 79 | return super.init(); 80 | } finally { 81 | _$ResultsStoreBaseActionController.endAction(_$actionInfo); 82 | } 83 | } 84 | 85 | @override 86 | String toString() { 87 | return ''' 88 | localResults: ${localResults}, 89 | globalResults: ${globalResults} 90 | '''; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /mobile/lib/store/settings_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:injectable/injectable.dart'; 2 | import 'package:mobx/mobx.dart'; 3 | import 'package:package_info/package_info.dart'; 4 | import 'package:pinger/config.dart'; 5 | import 'package:pinger/model/user_settings.dart'; 6 | import 'package:pinger/service/pinger_prefs.dart'; 7 | 8 | part 'settings_store.g.dart'; 9 | 10 | @singleton 11 | class SettingsStore extends SettingsStoreBase with _$SettingsStore { 12 | SettingsStore(this._packageInfo, this._pingerPrefs, this._appConfig); 13 | 14 | @override 15 | final PackageInfo _packageInfo; 16 | @override 17 | final PingerPrefs _pingerPrefs; 18 | @override 19 | final AppConfig _appConfig; 20 | } 21 | 22 | abstract class SettingsStoreBase with Store { 23 | PackageInfo get _packageInfo; 24 | PingerPrefs get _pingerPrefs; 25 | AppConfig get _appConfig; 26 | 27 | @observable 28 | UserSettings? userSettings; 29 | 30 | @observable 31 | bool? didShowIntro; 32 | 33 | @observable 34 | AppInfo? appInfo; 35 | 36 | @observable 37 | String privacyPolicyUrl = ''; 38 | 39 | @action 40 | Future init() async { 41 | didShowIntro = _pingerPrefs.getDidShowIntro() ?? false; 42 | userSettings = _getUserSettings(); 43 | privacyPolicyUrl = 'https://gist.github.com/tomwyr/9e530eb3ad2957c2d0f6c91dec30460e'; 44 | appInfo = AppInfo( 45 | name: _packageInfo.appName, 46 | version: _packageInfo.version, 47 | icon: _appConfig.iconPath, 48 | copyright: "© 2023 Tomasz Wyrowiński", 49 | ); 50 | } 51 | 52 | UserSettings? _getUserSettings() { 53 | var settings = _pingerPrefs.getUserSettings(); 54 | if (settings == null) { 55 | settings = _createDefaultSettings(); 56 | _pingerPrefs.saveUserSettings(settings); 57 | } else if (settings.traySettings == null) { 58 | settings = settings.copyWith(traySettings: _createDefaultTraySettings()); 59 | _pingerPrefs.saveUserSettings(settings); 60 | } 61 | return settings; 62 | } 63 | 64 | UserSettings _createDefaultSettings() => UserSettings( 65 | nightMode: false, 66 | restoreHost: false, 67 | showSystemNotification: false, 68 | traySettings: _createDefaultTraySettings(), 69 | shareSettings: ShareSettings( 70 | shareResults: true, 71 | attachLocation: false, 72 | ), 73 | pingSettings: PingSettings( 74 | count: const NumSetting.finite(value: 10), 75 | packetSize: 56, 76 | interval: 1, 77 | timeout: 10, 78 | ), 79 | ); 80 | 81 | TraySettings _createDefaultTraySettings() => TraySettings(enabled: true, autoReveal: false); 82 | 83 | @action 84 | Future updateSettings(UserSettings settings) async { 85 | await _pingerPrefs.saveUserSettings(settings); 86 | userSettings = settings; 87 | } 88 | 89 | @action 90 | Future notifyDidShowIntro() async { 91 | await _pingerPrefs.setDidShowIntro(true); 92 | didShowIntro = true; 93 | } 94 | } 95 | 96 | class AppInfo { 97 | AppInfo({ 98 | required this.version, 99 | required this.name, 100 | required this.icon, 101 | required this.copyright, 102 | }); 103 | 104 | final String version; 105 | final String name; 106 | final String copyright; 107 | final String icon; 108 | } 109 | -------------------------------------------------------------------------------- /mobile/lib/ui/app/pinger_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_localizations/flutter_localizations.dart'; 3 | import 'package:flutter_mobx/flutter_mobx.dart'; 4 | import 'package:mobx/mobx.dart'; 5 | import 'package:pinger/di/injector.dart'; 6 | import 'package:pinger/generated/l10n.dart'; 7 | import 'package:pinger/resources.dart'; 8 | import 'package:pinger/store/hosts_store.dart'; 9 | import 'package:pinger/store/settings_store.dart'; 10 | import 'package:pinger/ui/app/pinger_router.dart'; 11 | import 'package:pinger/ui/info_tray/info_tray.dart'; 12 | import 'package:pinger/ui/permissions_sheet.dart'; 13 | import 'package:pinger/ui/shared/tiles/host_icon_tile.dart'; 14 | 15 | class PingerApp extends StatefulWidget { 16 | const PingerApp({super.key}); 17 | 18 | static final PingerNavigatorRouter _router = PingerNavigatorRouter(); 19 | static PingerRouter get router => _router; 20 | 21 | @override 22 | State createState() => _PingerAppState(); 23 | } 24 | 25 | class _PingerAppState extends State { 26 | final SettingsStore _settingsStore = Injector.resolve(); 27 | final HostsStore _hostsStore = Injector.resolve(); 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | reaction( 33 | (_) => _settingsStore.userSettings!.nightMode, 34 | (it) => setState(() => R.load(it ? Brightness.dark : Brightness.light)), 35 | fireImmediately: true, 36 | ); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return Observer( 42 | builder: (_) => MaterialApp( 43 | localizationsDelegates: const [ 44 | GlobalWidgetsLocalizations.delegate, 45 | GlobalMaterialLocalizations.delegate, 46 | S.delegate, 47 | ], 48 | initialRoute: PingerRoutes.init, 49 | onGenerateRoute: PingerApp._router.generateRoute, 50 | navigatorObservers: [PingerApp._router], 51 | supportedLocales: S.delegate.supportedLocales, 52 | theme: R.themes.app, 53 | themeMode: R.themes.mode, 54 | title: _settingsStore.appInfo!.name, 55 | builder: (_, child) => PermissionsSheet( 56 | child: HostIconProvider( 57 | getIcon: _hostsStore.getFavicon, 58 | child: InfoTray(child: child), 59 | ), 60 | ), 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/animated_ink_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AnimatedInkIcon extends StatefulWidget { 4 | const AnimatedInkIcon({ 5 | super.key, 6 | required this.icon, 7 | required this.transition, 8 | required this.onPressed, 9 | }); 10 | 11 | final AnimatedIconData icon; 12 | final bool transition; 13 | final VoidCallback onPressed; 14 | 15 | @override 16 | State createState() => _AnimatedInkIconState(); 17 | } 18 | 19 | class _AnimatedInkIconState extends State with SingleTickerProviderStateMixin { 20 | late AnimationController _animator; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _animator = AnimationController( 26 | vsync: this, 27 | duration: const Duration(milliseconds: 300), 28 | value: widget.transition ? 1.0 : 0.0, 29 | ); 30 | } 31 | 32 | @override 33 | void didUpdateWidget(AnimatedInkIcon old) { 34 | super.didUpdateWidget(old); 35 | if (old.transition != widget.transition) { 36 | widget.transition ? _animator.forward() : _animator.reverse(); 37 | } 38 | } 39 | 40 | @override 41 | void dispose() { 42 | _animator.dispose(); 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return InkResponse( 49 | highlightShape: BoxShape.circle, 50 | containedInkWell: true, 51 | radius: 0.0, 52 | onTap: widget.onPressed, 53 | child: Padding( 54 | padding: const EdgeInsets.all(16.0), 55 | child: AnimatedIcon(icon: widget.icon, progress: _animator), 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/box_clipper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ClipBox extends StatelessWidget { 4 | const ClipBox({ 5 | super.key, 6 | this.width, 7 | this.height, 8 | required this.child, 9 | }); 10 | 11 | final double? width; 12 | final double? height; 13 | final Widget child; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return ClipRect( 18 | clipper: BoxClipper(width, height), 19 | child: child, 20 | ); 21 | } 22 | } 23 | 24 | class BoxClipper extends CustomClipper { 25 | BoxClipper(this.width, this.height); 26 | 27 | final double? width; 28 | final double? height; 29 | 30 | @override 31 | Rect getClip(Size size) => Rect.fromLTWH(0.0, 0.0, width ?? size.width, height ?? size.height); 32 | 33 | @override 34 | bool shouldReclip(BoxClipper oldClipper) => 35 | oldClipper.width != width || oldClipper.height != height; 36 | } 37 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/collapsing_tab_layout.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CollapsingTabLayout extends StatefulWidget { 4 | const CollapsingTabLayout({ 5 | super.key, 6 | required this.appBar, 7 | required this.tabBarView, 8 | required this.collapsedOffset, 9 | required this.controller, 10 | required this.scrollLayout, 11 | }); 12 | 13 | final SliverAppBar appBar; 14 | final TabBarView tabBarView; 15 | final double collapsedOffset; 16 | final ScrollController? controller; 17 | final Future Function(double) scrollLayout; 18 | 19 | @override 20 | State createState() => _CollapsingTabLayoutState(); 21 | } 22 | 23 | class _CollapsingTabLayoutState extends State { 24 | bool _isSnapping = false; 25 | ScrollUpdateNotification? _lastScrollUpdate; 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return NotificationListener( 30 | onNotification: _onScrollEvent, 31 | child: NestedScrollView( 32 | controller: widget.controller, 33 | headerSliverBuilder: (headerContext, __) => [ 34 | SliverOverlapAbsorber( 35 | handle: NestedScrollView.sliverOverlapAbsorberHandleFor( 36 | headerContext, 37 | ), 38 | sliver: widget.appBar, 39 | ), 40 | ], 41 | body: widget.tabBarView, 42 | ), 43 | ); 44 | } 45 | 46 | bool _onScrollEvent(Notification notification) { 47 | if (_isSnapping) return false; 48 | if (notification is ScrollUpdateNotification) { 49 | _lastScrollUpdate = notification; 50 | } else if (notification is ScrollEndNotification) { 51 | if (_lastScrollUpdate != null && 52 | widget.controller!.offset > 0.0 && 53 | widget.controller!.offset < widget.collapsedOffset) { 54 | _isSnapping = true; 55 | _scheduleSnapScroll(_lastScrollUpdate!.scrollDelta! > 0.0); 56 | return true; 57 | } 58 | } 59 | return false; 60 | } 61 | 62 | void _scheduleSnapScroll(bool scrollsUpwards) { 63 | Future(() async { 64 | await widget.scrollLayout(scrollsUpwards ? widget.collapsedOffset : 0.0); 65 | _lastScrollUpdate = null; 66 | _isSnapping = false; 67 | }); 68 | } 69 | } 70 | 71 | class CollapsingTabLayoutItem extends StatelessWidget { 72 | const CollapsingTabLayoutItem({ 73 | super.key, 74 | required this.slivers, 75 | this.controller, 76 | }); 77 | 78 | final List slivers; 79 | final ScrollController? controller; 80 | 81 | @override 82 | Widget build(BuildContext context) { 83 | return CustomScrollView( 84 | controller: controller, 85 | slivers: [ 86 | SliverOverlapInjector( 87 | handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), 88 | ), 89 | ...slivers, 90 | ], 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/collapsing_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/ui/common/inline_multi_child_layout_delegate.dart'; 3 | 4 | class CollapsingTile extends StatelessWidget { 5 | const CollapsingTile({ 6 | super.key, 7 | required this.expansion, 8 | required this.avatar, 9 | required this.label, 10 | }); 11 | 12 | final double expansion; 13 | final Widget avatar; 14 | final Widget label; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return CustomMultiChildLayout( 19 | delegate: InlineMultiChildLayoutDelegate( 20 | config: expansion, 21 | performLayout: (size, self) { 22 | final constraints = BoxConstraints.loose(size); 23 | final avatarSize = self.layoutChild(CollapsingTileItem.avatar, constraints); 24 | final labelSize = self.layoutChild(CollapsingTileItem.label, constraints); 25 | final totalSize = 26 | Size(avatarSize.width + labelSize.width, avatarSize.height + labelSize.height); 27 | final avatarX = (constraints.maxWidth - 28 | (totalSize.width - avatarSize.width) * (1 - expansion) - 29 | avatarSize.width) / 30 | 2; 31 | final labelX = 32 | (constraints.maxWidth - labelSize.width + avatarSize.width * (1 - expansion)) / 2; 33 | const avatarY = 0.0; 34 | final labelY = avatarSize.height * expansion; 35 | self.positionChild(CollapsingTileItem.avatar, Offset(avatarX, avatarY)); 36 | self.positionChild(CollapsingTileItem.label, Offset(labelX, labelY)); 37 | }, 38 | ), 39 | children: [ 40 | LayoutId(id: CollapsingTileItem.avatar, child: avatar), 41 | LayoutId(id: CollapsingTileItem.label, child: label), 42 | ], 43 | ); 44 | } 45 | } 46 | 47 | enum CollapsingTileItem { avatar, label } 48 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/fade_out.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FadeOut extends StatefulWidget { 4 | const FadeOut({ 5 | super.key, 6 | required this.duration, 7 | required this.visible, 8 | required this.child, 9 | }); 10 | 11 | final Duration duration; 12 | final bool visible; 13 | final Widget child; 14 | 15 | @override 16 | State createState() => _FadeOutState(); 17 | } 18 | 19 | class _FadeOutState extends State with SingleTickerProviderStateMixin { 20 | late AnimationController _animator; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _animator = AnimationController( 26 | vsync: this, 27 | duration: widget.duration, 28 | value: widget.visible ? 1.0 : 0.0, 29 | ); 30 | } 31 | 32 | @override 33 | void didUpdateWidget(FadeOut old) { 34 | super.didUpdateWidget(old); 35 | if (old.visible != widget.visible) { 36 | widget.visible ? _animator.forward() : _animator.reverse(); 37 | } 38 | } 39 | 40 | @override 41 | void dispose() { 42 | _animator.dispose(); 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return SizeTransition( 49 | sizeFactor: _animator, 50 | child: FadeTransition( 51 | opacity: _animator, 52 | child: widget.child, 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/flex_child_scroll_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FlexChildScrollView extends StatelessWidget { 4 | const FlexChildScrollView({ 5 | super.key, 6 | this.controller, 7 | required this.child, 8 | }); 9 | 10 | final ScrollController? controller; 11 | final Widget child; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return LayoutBuilder( 16 | builder: (_, constraints) => SingleChildScrollView( 17 | controller: controller, 18 | child: ConstrainedBox( 19 | constraints: BoxConstraints(minHeight: constraints.maxHeight), 20 | child: child, 21 | ), 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/inline_multi_child_layout_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/rendering.dart'; 2 | 3 | typedef InlinePerformLayout = void Function(Size size, MultiChildLayoutDelegate self); 4 | typedef InlineShouldRelayout = bool Function(InlineMultiChildLayoutDelegate old); 5 | 6 | class InlineMultiChildLayoutDelegate extends MultiChildLayoutDelegate { 7 | InlineMultiChildLayoutDelegate._( 8 | this.performLayoutDelegate, 9 | this.shouldRelayoutDelegate, 10 | this.config, 11 | ); 12 | 13 | factory InlineMultiChildLayoutDelegate({ 14 | required InlinePerformLayout performLayout, 15 | InlineShouldRelayout? shouldRelayout, 16 | required T config, 17 | }) { 18 | shouldRelayout ??= (old) => old.config != config; 19 | return InlineMultiChildLayoutDelegate._(performLayout, shouldRelayout, config); 20 | } 21 | 22 | final InlinePerformLayout performLayoutDelegate; 23 | final InlineShouldRelayout shouldRelayoutDelegate; 24 | final T config; 25 | 26 | @override 27 | void performLayout(Size size) => performLayoutDelegate(size, this); 28 | 29 | @override 30 | bool shouldRelayout(InlineMultiChildLayoutDelegate oldDelegate) => 31 | shouldRelayoutDelegate(oldDelegate); 32 | } 33 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/separated_sliver_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SeparatedSliverList extends StatelessWidget { 4 | const SeparatedSliverList({ 5 | super.key, 6 | required this.itemCount, 7 | required this.itemBuilder, 8 | required this.separatorBuilder, 9 | }); 10 | 11 | final int itemCount; 12 | final IndexedWidgetBuilder itemBuilder; 13 | final IndexedWidgetBuilder separatorBuilder; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return SliverList( 18 | delegate: SliverChildBuilderDelegate( 19 | (context, index) { 20 | final builder = !index.isEven ? separatorBuilder : itemBuilder; 21 | return builder(context, index ~/ 2); 22 | }, 23 | semanticIndexCallback: (_, localIndex) => localIndex.isEven ? localIndex ~/ 2 : null, 24 | childCount: itemCount > 0 ? (2 * itemCount - 1) : 0, 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/snapping_scroll_area.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SnappingScrollArea extends StatefulWidget { 4 | const SnappingScrollArea({ 5 | super.key, 6 | required this.child, 7 | required this.itemInterval, 8 | required this.itemCount, 9 | required this.onPositionChanged, 10 | this.scrollDirection = Axis.horizontal, 11 | this.snapDuration = const Duration(milliseconds: 300), 12 | this.mainAxisPadding = 0.0, 13 | this.initialPosition = 0, 14 | }); 15 | 16 | final int initialPosition; 17 | final int itemCount; 18 | final Widget child; 19 | final double itemInterval; 20 | final double mainAxisPadding; 21 | final Axis scrollDirection; 22 | final Duration snapDuration; 23 | final ValueChanged onPositionChanged; 24 | 25 | @override 26 | State createState() => _SnappingScrollAreaState(); 27 | } 28 | 29 | class _SnappingScrollAreaState extends State { 30 | ScrollController? _scrollController; 31 | int? _snapPosition; 32 | bool _isSnapping = false; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | _snapPosition = widget.initialPosition; 38 | _scrollController = ScrollController( 39 | initialScrollOffset: _calcPositionOffset(_snapPosition!), 40 | ); 41 | } 42 | 43 | @override 44 | void dispose() { 45 | _scrollController!.dispose(); 46 | super.dispose(); 47 | } 48 | 49 | bool _onScrollEnd(ScrollEndNotification notification) { 50 | if (!_isSnapping) { 51 | final relativeOffset = 52 | _scrollController!.offset + (widget.itemInterval - widget.mainAxisPadding) / 2; 53 | final position = (relativeOffset ~/ widget.itemInterval).clamp(0, widget.itemCount - 1); 54 | Future(() => _snapChartTo(position)); 55 | if (position != _snapPosition) { 56 | _snapPosition = position; 57 | widget.onPositionChanged(position); 58 | } 59 | } 60 | return true; 61 | } 62 | 63 | void _snapChartTo(int position) async { 64 | final offset = _calcPositionOffset(position); 65 | if (offset != _scrollController!.offset) { 66 | _isSnapping = true; 67 | await _scrollController!.animateTo( 68 | offset, 69 | duration: widget.snapDuration, 70 | curve: Curves.easeOut, 71 | ); 72 | _isSnapping = false; 73 | } 74 | } 75 | 76 | double _calcPositionOffset(int position) => 77 | position * widget.itemInterval + widget.mainAxisPadding / 2; 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | return NotificationListener( 82 | onNotification: _onScrollEnd, 83 | child: SingleChildScrollView( 84 | scrollDirection: widget.scrollDirection, 85 | controller: _scrollController, 86 | child: Container( 87 | width: (widget.itemCount - 1) * widget.itemInterval + 2 * widget.mainAxisPadding, 88 | padding: EdgeInsets.symmetric(horizontal: widget.mainAxisPadding), 89 | child: OverflowBox( 90 | alignment: Alignment.centerLeft, 91 | child: widget.child, 92 | ), 93 | ), 94 | ), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /mobile/lib/ui/common/transparent_gradient_box.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TransparentGradientBox extends StatelessWidget { 4 | const TransparentGradientBox._({ 5 | super.key, 6 | required this.color, 7 | required this.width, 8 | required this.height, 9 | required this.beginAlignment, 10 | required this.endAlignment, 11 | }); 12 | 13 | factory TransparentGradientBox({ 14 | Key? key, 15 | required Color color, 16 | required AxisDirection direction, 17 | double size = double.infinity, 18 | }) => 19 | TransparentGradientBox._( 20 | key: key, 21 | color: color, 22 | width: direction == AxisDirection.left || direction == AxisDirection.right 23 | ? size 24 | : double.infinity, 25 | height: direction == AxisDirection.up || direction == AxisDirection.down 26 | ? size 27 | : double.infinity, 28 | beginAlignment: direction == AxisDirection.up 29 | ? Alignment.bottomCenter 30 | : direction == AxisDirection.down 31 | ? Alignment.topCenter 32 | : direction == AxisDirection.left 33 | ? Alignment.centerRight 34 | : Alignment.centerLeft, 35 | endAlignment: direction == AxisDirection.up 36 | ? Alignment.topCenter 37 | : direction == AxisDirection.down 38 | ? Alignment.bottomCenter 39 | : direction == AxisDirection.left 40 | ? Alignment.centerLeft 41 | : Alignment.centerRight, 42 | ); 43 | 44 | final Color color; 45 | final double width; 46 | final double height; 47 | final Alignment beginAlignment; 48 | final Alignment endAlignment; 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return SizedBox( 53 | width: width, 54 | height: height, 55 | child: TweenAnimationBuilder( 56 | tween: ColorTween(begin: color, end: color), 57 | duration: kThemeChangeDuration, 58 | builder: (_, dynamic value, __) => DecoratedBox( 59 | decoration: BoxDecoration( 60 | gradient: LinearGradient( 61 | colors: [value, value.withOpacity(0.0)], 62 | begin: beginAlignment, 63 | end: endAlignment, 64 | ), 65 | ), 66 | ), 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /mobile/lib/ui/info_tray/info_tray_entry.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_mobx/flutter_mobx.dart'; 4 | import 'package:mobx/mobx.dart'; 5 | 6 | import 'package:pinger/ui/common/draggable_sheet.dart'; 7 | import 'package:pinger/ui/info_tray/info_tray.dart'; 8 | 9 | class InfoTrayEntry implements SeparatedItem { 10 | InfoTrayEntry({ 11 | required this.item, 12 | required this.valueObservable, 13 | required this.valueBuilder, 14 | required this.isVisible, 15 | }); 16 | 17 | final InfoTrayItem item; 18 | final ValueGetter valueObservable; 19 | final Widget Function(T value) valueBuilder; 20 | final bool Function(T value) isVisible; 21 | 22 | @override 23 | final ValueNotifier visibility = ValueNotifier(null); 24 | 25 | late ReactionDisposer _reactionDisposer; 26 | 27 | void init() { 28 | _reactionDisposer = reaction( 29 | (_) => isVisible(valueObservable()), 30 | (dynamic it) => visibility.value = it, 31 | fireImmediately: true, 32 | ); 33 | } 34 | 35 | void dispose() { 36 | _reactionDisposer(); 37 | visibility.dispose(); 38 | } 39 | 40 | @override 41 | InfoTrayItem get value => item; 42 | 43 | @override 44 | WidgetBuilder get builder => (_) => Observer(builder: (_) => valueBuilder(valueObservable())); 45 | } 46 | -------------------------------------------------------------------------------- /mobile/lib/ui/info_tray/items/info_tray_connectivity_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/generated/l10n.dart'; 3 | import 'package:pinger/resources.dart'; 4 | 5 | class InfoTrayConnectivityItem extends StatelessWidget { 6 | const InfoTrayConnectivityItem({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Material( 11 | color: R.colors.none, 12 | child: Padding( 13 | padding: const EdgeInsets.symmetric(horizontal: 8.0), 14 | child: Row(children: [ 15 | Expanded( 16 | child: Text( 17 | S.current.infoTrayNetworkDisabled, 18 | style: TextStyle(color: R.colors.white), 19 | ), 20 | ), 21 | Container(width: 16.0), 22 | Icon(Icons.signal_wifi_off, color: R.colors.white), 23 | ]), 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/base_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | abstract class BaseState extends State { 4 | @override 5 | void didChangeDependencies() { 6 | super.didChangeDependencies(); 7 | // Reference theme in order to rebuild when app changes to dark/light mode 8 | Theme.of(context); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/favorites_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:pinger/di/injector.dart'; 4 | import 'package:pinger/generated/l10n.dart'; 5 | import 'package:pinger/store/hosts_store.dart'; 6 | import 'package:pinger/store/ping_store.dart'; 7 | import 'package:pinger/ui/page/base_page.dart'; 8 | import 'package:pinger/ui/page/hosts_page.dart'; 9 | import 'package:pinger/utils/host_tap_handler.dart'; 10 | 11 | class FavoritesPage extends StatefulWidget { 12 | const FavoritesPage({super.key}); 13 | 14 | @override 15 | State createState() => _FavoritesPageState(); 16 | } 17 | 18 | class _FavoritesPageState extends BaseState with HostTapHandler { 19 | final PingStore _pingStore = Injector.resolve(); 20 | final HostsStore _hostsStore = Injector.resolve(); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Observer(builder: (_) { 25 | final stats = _hostsStore.localStats; 26 | final hosts = _hostsStore.favorites!.toList() 27 | ..sort((e1, e2) { 28 | if (!stats!.containsKey(e1)) return 1; 29 | if (!stats.containsKey(e2)) return -1; 30 | return stats[e2]!.pingCount.compareTo(stats[e1]!.pingCount); 31 | }); 32 | return HostsPage( 33 | title: S.current.favoritesPageTitle, 34 | hosts: hosts, 35 | getTrailingLabel: (it) => S.current.pingCountLabel(stats![it]?.pingCount ?? 0), 36 | removeHosts: _hostsStore.removeFavorites, 37 | onHostSelected: (it) => onHostTap(_pingStore, it), 38 | ); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/init_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/di/injector.dart'; 3 | import 'package:pinger/resources.dart'; 4 | import 'package:pinger/store/ping_store.dart'; 5 | import 'package:pinger/ui/app/pinger_app.dart'; 6 | import 'package:pinger/ui/app/pinger_router.dart'; 7 | import 'package:pinger/ui/page/base_page.dart'; 8 | 9 | class InitPage extends StatefulWidget { 10 | const InitPage({super.key}); 11 | 12 | @override 13 | InitPageState createState() => InitPageState(); 14 | } 15 | 16 | class InitPageState extends BaseState { 17 | @override 18 | void initState() { 19 | super.initState(); 20 | Future(() { 21 | PingerApp.router.show(RouteConfig.home()); 22 | final hasSession = Injector.resolve().currentSession != null; 23 | if (hasSession) PingerApp.router.show(RouteConfig.session()); 24 | }); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Container(color: R.colors.canvas); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/recents_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:pinger/di/injector.dart'; 4 | import 'package:pinger/generated/l10n.dart'; 5 | import 'package:pinger/store/hosts_store.dart'; 6 | import 'package:pinger/store/ping_store.dart'; 7 | import 'package:pinger/ui/page/base_page.dart'; 8 | import 'package:pinger/ui/page/hosts_page.dart'; 9 | import 'package:pinger/utils/format_utils.dart'; 10 | import 'package:pinger/utils/host_tap_handler.dart'; 11 | 12 | class RecentsPage extends StatefulWidget { 13 | const RecentsPage({super.key}); 14 | 15 | @override 16 | State createState() => _RecentsPageState(); 17 | } 18 | 19 | class _RecentsPageState extends BaseState with HostTapHandler { 20 | final HostsStore _hostsStore = Injector.resolve(); 21 | final PingStore _pingStore = Injector.resolve(); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Observer(builder: (_) { 26 | final stats = _hostsStore.localStats!; 27 | return HostsPage( 28 | title: S.current.recentsPageTitle, 29 | hosts: stats.keys.toList(), 30 | getTrailingLabel: (it) => FormatUtils.getSinceNowLabel(stats[it]!.pingTime), 31 | removeHosts: _hostsStore.removeStats, 32 | onHostSelected: (it) => onHostTap(_pingStore, it), 33 | ); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/result_details/result_details_tab/result_details_info_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/generated/l10n.dart'; 3 | import 'package:pinger/model/ping_result.dart'; 4 | import 'package:pinger/resources.dart'; 5 | import 'package:pinger/ui/common/collapsing_tab_layout.dart'; 6 | import 'package:pinger/utils/format_utils.dart'; 7 | 8 | class ResultDetailsInfoTab extends StatelessWidget { 9 | const ResultDetailsInfoTab({ 10 | super.key, 11 | required this.result, 12 | }); 13 | 14 | final PingResult? result; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return CollapsingTabLayoutItem(slivers: [ 19 | SliverToBoxAdapter( 20 | child: Padding( 21 | padding: const EdgeInsets.all(24.0), 22 | child: Column( 23 | mainAxisSize: MainAxisSize.min, 24 | crossAxisAlignment: CrossAxisAlignment.start, 25 | children: [ 26 | _buildHeader(S.current.pingInfoInfoSubtitle), 27 | _buildItem( 28 | S.current.pingInfoDateLabel, 29 | FormatUtils.getTimestampLabel(result!.startTime, showTime: true), 30 | ), 31 | _buildItem( 32 | S.current.pingInfoDurationLabel, 33 | FormatUtils.getDurationLabel(result!.duration), 34 | ), 35 | Container(height: 24.0), 36 | _buildHeader(S.current.pingInfoSettingsSubtitle), 37 | _buildItem( 38 | S.current.settingsPingCountLabel, 39 | "${FormatUtils.getCountLabel(result!.settings.count)} ${S.current.settingsPingCountUnit}", 40 | ), 41 | _buildItem( 42 | S.current.settingsPingPacketSizeLabel, 43 | "${result!.settings.packetSize} ${S.current.settingsPingPacketSizeUnit}", 44 | ), 45 | _buildItem( 46 | S.current.settingsPingIntervalLabel, 47 | "${result!.settings.interval} ${S.current.settingsPingIntervalUnit}", 48 | ), 49 | _buildItem( 50 | S.current.settingsPingTimeoutLabel, 51 | "${result!.settings.timeout} ${S.current.settingsPingTimeoutUnit}", 52 | ), 53 | ], 54 | ), 55 | ), 56 | ) 57 | ]); 58 | } 59 | 60 | Widget _buildHeader(String name) { 61 | return Padding( 62 | padding: const EdgeInsets.only(bottom: 8.0), 63 | child: Text( 64 | name, 65 | style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18.0), 66 | ), 67 | ); 68 | } 69 | 70 | Widget _buildItem(String name, String value) { 71 | return Padding( 72 | padding: const EdgeInsets.symmetric(vertical: 6.0), 73 | child: Row(children: [ 74 | Text(name, style: TextStyle(color: R.colors.gray, fontSize: 18.0)), 75 | const Spacer(), 76 | Text(value, style: const TextStyle(fontSize: 18.0)), 77 | ]), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/result_details/result_details_tab/result_details_more_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/assets.dart'; 3 | import 'package:pinger/generated/l10n.dart'; 4 | import 'package:pinger/model/ping_result.dart'; 5 | import 'package:pinger/resources.dart'; 6 | import 'package:pinger/ui/common/collapsing_tab_layout.dart'; 7 | import 'package:pinger/ui/common/scroll_edge_gradient.dart'; 8 | import 'package:pinger/ui/common/separated_sliver_list.dart'; 9 | import 'package:pinger/ui/page/result_details/result_details_tab/result_details_prompt_tab.dart'; 10 | import 'package:pinger/ui/shared/tiles/result_tile.dart'; 11 | 12 | class ResultDetailsMoreTab extends StatefulWidget { 13 | const ResultDetailsMoreTab({ 14 | super.key, 15 | required this.results, 16 | required this.onItemSelected, 17 | required this.onStartPingPressed, 18 | }); 19 | 20 | final List results; 21 | final ValueChanged onItemSelected; 22 | final VoidCallback onStartPingPressed; 23 | 24 | @override 25 | State createState() => _ResultDetailsMoreTabState(); 26 | } 27 | 28 | class _ResultDetailsMoreTabState extends State { 29 | @override 30 | Widget build(BuildContext context) { 31 | return ScrollEdgeGradient( 32 | color: R.colors.canvas, 33 | sliverOverlap: kToolbarHeight + kTextTabBarHeight, 34 | builder: (controller) => CollapsingTabLayoutItem( 35 | controller: controller, 36 | slivers: [ 37 | if (widget.results.isEmpty) 38 | ResultDetailsPromptTab( 39 | image: Images.undrawEmpty, 40 | title: S.current.nothingToShowTitle, 41 | description: S.current.resultOtherEmptyDesc, 42 | buttonLabel: S.current.startNowButtonLabel, 43 | onButtonPressed: widget.onStartPingPressed, 44 | ) 45 | else 46 | SliverPadding( 47 | padding: const EdgeInsets.all(16.0), 48 | sliver: SeparatedSliverList( 49 | itemCount: widget.results.length, 50 | itemBuilder: (_, index) { 51 | final item = widget.results[index]; 52 | return ResultTile( 53 | result: item, 54 | type: ResultTileType.detailed, 55 | onPressed: () => widget.onItemSelected(item), 56 | ); 57 | }, 58 | separatorBuilder: (_, __) => const Divider(), 59 | ), 60 | ) 61 | ], 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/result_details/result_details_tab/result_details_prompt_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/ui/shared/info_section.dart'; 3 | 4 | class ResultDetailsPromptTab extends StatelessWidget { 5 | const ResultDetailsPromptTab({ 6 | super.key, 7 | required this.image, 8 | required this.title, 9 | required this.description, 10 | required this.buttonLabel, 11 | required this.onButtonPressed, 12 | }); 13 | 14 | final AssetImage image; 15 | final String title; 16 | final String description; 17 | final String buttonLabel; 18 | final VoidCallback onButtonPressed; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return SliverFillRemaining( 23 | hasScrollBody: false, 24 | child: Padding( 25 | padding: const EdgeInsets.symmetric(horizontal: 32.0), 26 | child: Column(children: [ 27 | Expanded( 28 | child: InfoSection( 29 | image: image, 30 | title: title, 31 | description: description, 32 | ), 33 | ), 34 | Padding( 35 | padding: const EdgeInsets.symmetric(vertical: 40.0), 36 | child: ElevatedButton( 37 | onPressed: onButtonPressed, 38 | child: Text(buttonLabel), 39 | ), 40 | ), 41 | ]), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/session/session_button/ping_floating_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/resources.dart'; 3 | 4 | class PingFloatingButton extends StatelessWidget { 5 | const PingFloatingButton({ 6 | super.key, 7 | required this.duration, 8 | required this.raised, 9 | required this.size, 10 | required this.icon, 11 | required this.iconColor, 12 | required this.backgroundColor, 13 | this.onTap, 14 | this.onLongPressStart, 15 | this.onLongPressEnd, 16 | this.onSwipeUpdate, 17 | this.onSwipeEnd, 18 | }); 19 | 20 | final Duration duration; 21 | final bool raised; 22 | final double size; 23 | final IconData icon; 24 | final Color iconColor; 25 | final Color backgroundColor; 26 | final VoidCallback? onTap; 27 | final VoidCallback? onLongPressStart; 28 | final VoidCallback? onLongPressEnd; 29 | final ValueChanged? onSwipeUpdate; 30 | final VoidCallback? onSwipeEnd; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | final shadowColor = raised ? R.colors.shadow : R.colors.none; 35 | return Listener( 36 | onPointerMove: onSwipeUpdate != null ? (it) => onSwipeUpdate!(it.delta.dx) : null, 37 | onPointerUp: onSwipeEnd != null ? (_) => onSwipeEnd!() : null, 38 | child: GestureDetector( 39 | onTap: onTap, 40 | onLongPressStart: onLongPressStart != null ? (_) => onLongPressStart!() : null, 41 | onLongPressEnd: onLongPressEnd != null ? (_) => onLongPressEnd!() : null, 42 | child: SizedBox.fromSize( 43 | size: Size.square(size), 44 | child: TweenAnimationBuilder( 45 | tween: ColorTween(begin: shadowColor, end: shadowColor), 46 | curve: Interval(raised ? 0.5 : 0.0, raised ? 1.0 : 0.5), 47 | duration: duration, 48 | builder: (_, value, __) => DecoratedBox( 49 | decoration: BoxDecoration( 50 | color: backgroundColor, 51 | borderRadius: BorderRadius.circular(size / 2), 52 | boxShadow: [ 53 | BoxShadow(color: value!, spreadRadius: 0.5, blurRadius: 4.0), 54 | ], 55 | ), 56 | child: Icon(icon, color: iconColor), 57 | ), 58 | ), 59 | ), 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/session/session_button/ping_lock_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class PingLockIndicatorDimens { 6 | PingLockIndicatorDimens({ 7 | this.arrowSize = const Size(8.0, 16.0), 8 | this.arrowCount = 4, 9 | this.iconSize = 18.0, 10 | this.iconMargin = 8.0, 11 | }) : totalWidth = arrowCount * arrowSize.width + iconSize + iconMargin; 12 | 13 | final Size arrowSize; 14 | final double arrowCount; 15 | final double iconSize; 16 | final double iconMargin; 17 | final double totalWidth; 18 | } 19 | 20 | class PingLockIndicator extends StatelessWidget { 21 | const PingLockIndicator({ 22 | super.key, 23 | required this.duration, 24 | required this.direction, 25 | required this.margin, 26 | required this.isLocked, 27 | required this.color, 28 | required this.dimens, 29 | required this.swipe, 30 | }); 31 | 32 | final Duration duration; 33 | final TextDirection direction; 34 | final double margin; 35 | final bool? isLocked; 36 | final ColorTween color; 37 | final PingLockIndicatorDimens dimens; 38 | final ValueNotifier swipe; 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return Container( 43 | margin: EdgeInsets.only( 44 | left: direction == TextDirection.ltr ? margin : 0.0, 45 | right: direction == TextDirection.rtl ? margin : 0.0, 46 | ), 47 | child: AnimatedOpacity( 48 | duration: duration, 49 | opacity: isLocked != null ? 1.0 : 0.0, 50 | child: ValueListenableBuilder( 51 | valueListenable: swipe, 52 | builder: (_, value, __) => _buildIndicatorRow(value!), 53 | ), 54 | ), 55 | ); 56 | } 57 | 58 | Widget _buildIndicatorRow(double value) { 59 | return Row( 60 | mainAxisSize: MainAxisSize.min, 61 | textDirection: direction, 62 | children: [ 63 | for (var i = 0; i < dimens.arrowCount; i++) 64 | Transform.rotate( 65 | angle: direction == TextDirection.ltr ? 0.0 : pi, 66 | child: SizedBox( 67 | width: dimens.arrowSize.width, 68 | child: Icon( 69 | Icons.chevron_right, 70 | size: dimens.arrowSize.height, 71 | color: _calcLockArrowColor(i, value), 72 | ), 73 | ), 74 | ), 75 | Container(width: dimens.iconMargin), 76 | Icon( 77 | Icons.lock_outline, 78 | size: dimens.iconSize, 79 | color: _calcLockIconColor(value), 80 | ), 81 | ], 82 | ); 83 | } 84 | 85 | Color? _calcLockArrowColor(int index, double value) { 86 | return color.transform(Interval( 87 | index / (dimens.arrowCount + 1), 88 | (index + 1) / (dimens.arrowCount + 1), 89 | ).transform(value)); 90 | } 91 | 92 | Color? _calcLockIconColor(double value) { 93 | return color.transform(Interval( 94 | dimens.arrowCount / (dimens.arrowCount + 1), 95 | 1.0, 96 | ).transform(value)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/session/session_host_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/resources.dart'; 3 | 4 | class SessionHostButton extends StatelessWidget { 5 | const SessionHostButton({ 6 | super.key, 7 | required this.icon, 8 | required this.label, 9 | required this.enabled, 10 | required this.active, 11 | this.onPressed, 12 | }); 13 | 14 | final IconData icon; 15 | final String label; 16 | final bool enabled; 17 | final bool active; 18 | final VoidCallback? onPressed; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final color = active ? R.colors.secondary : R.colors.gray; 23 | return Stack(children: [ 24 | GestureDetector( 25 | onTap: onPressed, 26 | child: Column( 27 | mainAxisSize: MainAxisSize.min, 28 | children: [ 29 | SizedBox.fromSize( 30 | size: const Size.square(40.0), 31 | child: OutlinedButton( 32 | style: OutlinedButton.styleFrom( 33 | padding: EdgeInsets.zero, 34 | shape: const CircleBorder(), 35 | side: BorderSide(color: color), 36 | foregroundColor: color, 37 | ), 38 | onPressed: onPressed, 39 | child: Icon(icon, color: color), 40 | ), 41 | ), 42 | Container(height: 8.0), 43 | Text(label, style: TextStyle(color: color)), 44 | ], 45 | ), 46 | ), 47 | if (!enabled) 48 | Positioned.fill( 49 | child: AbsorbPointer( 50 | child: Container( 51 | color: R.colors.canvas.withOpacity(0.75), 52 | ), 53 | ), 54 | ), 55 | ]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/session/session_host_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/generated/l10n.dart'; 3 | import 'package:pinger/model/ping_session.dart'; 4 | import 'package:pinger/ui/common/collapsing_header.dart'; 5 | import 'package:pinger/ui/shared/tiles/host_icon_tile.dart'; 6 | 7 | class SessionHostHeader extends StatelessWidget { 8 | const SessionHostHeader({ 9 | super.key, 10 | required this.session, 11 | required this.isExpanded, 12 | required this.buttons, 13 | required this.animDuration, 14 | }); 15 | 16 | final PingSession session; 17 | final bool isExpanded; 18 | final Duration animDuration; 19 | final List buttons; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final fontSize = isExpanded ? 24.0 : 18.0; 24 | return CollapsingHeader( 25 | isExpanded: isExpanded, 26 | duration: animDuration, 27 | title: S.current.sessionPageTitle, 28 | builder: (_, expansion, content) => Padding( 29 | padding: EdgeInsets.only( 30 | left: 32.0 * expansion, 31 | right: 12.0 + 20 * expansion, 32 | ), 33 | child: content, 34 | ), 35 | content: Row( 36 | mainAxisAlignment: MainAxisAlignment.center, 37 | children: [ 38 | Container(width: 8.0), 39 | HostIconTile(host: session.host, isRaised: isExpanded), 40 | Container(width: 20.0), 41 | Flexible( 42 | child: TweenAnimationBuilder( 43 | duration: animDuration, 44 | tween: Tween(begin: fontSize, end: fontSize), 45 | builder: (_, value, __) => Text( 46 | session.host, 47 | maxLines: 1, 48 | softWrap: false, 49 | overflow: TextOverflow.fade, 50 | style: TextStyle(fontSize: value), 51 | ), 52 | ), 53 | ), 54 | Container(width: 8.0), 55 | ], 56 | ), 57 | bottom: Padding( 58 | padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 24.0), 59 | child: Row( 60 | mainAxisAlignment: MainAxisAlignment.spaceAround, 61 | children: buttons, 62 | ), 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /mobile/lib/ui/page/settings/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:pinger/di/injector.dart'; 4 | import 'package:pinger/generated/l10n.dart'; 5 | import 'package:pinger/resources.dart'; 6 | import 'package:pinger/store/settings_store.dart'; 7 | import 'package:pinger/ui/app/pinger_app.dart'; 8 | import 'package:pinger/ui/app/pinger_router.dart'; 9 | import 'package:pinger/ui/common/scroll_edge_gradient.dart'; 10 | import 'package:pinger/ui/page/base_page.dart'; 11 | import 'package:pinger/ui/page/settings/settings_sections.dart'; 12 | import 'package:url_launcher/url_launcher_string.dart'; 13 | 14 | class SettingsPage extends StatefulWidget { 15 | const SettingsPage({super.key}); 16 | 17 | @override 18 | State createState() => _SettingsPageState(); 19 | } 20 | 21 | class _SettingsPageState extends BaseState { 22 | final SettingsStore _settingsStore = Injector.resolve(); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Scaffold( 27 | appBar: AppBar( 28 | leading: const CloseButton(), 29 | title: Text(S.current.settingsPageTitle), 30 | centerTitle: true, 31 | ), 32 | body: Observer(builder: (_) { 33 | final settings = _settingsStore.userSettings; 34 | final appInfo = _settingsStore.appInfo; 35 | if (settings == null) return Container(); 36 | return ScrollEdgeGradient( 37 | color: R.colors.canvas, 38 | builder: (controller) => ListView( 39 | controller: controller, 40 | padding: const EdgeInsets.fromLTRB(32.0, 16.0, 24.0, 16.0), 41 | children: [ 42 | PingSettingsSection( 43 | settings: settings.pingSettings, 44 | onChanged: (it) => _settingsStore.updateSettings( 45 | settings.copyWith(pingSettings: it), 46 | ), 47 | ), 48 | Container(height: 24.0), 49 | ShareSettingsSection( 50 | settings: settings.shareSettings, 51 | onChanged: (it) => _settingsStore.updateSettings( 52 | settings.copyWith(shareSettings: it), 53 | ), 54 | ), 55 | Container(height: 24.0), 56 | TraySettingsSection( 57 | settings: settings.traySettings, 58 | onChanged: (it) => _settingsStore.updateSettings( 59 | settings.copyWith(traySettings: it), 60 | ), 61 | ), 62 | Container(height: 24.0), 63 | OtherSettingsSection( 64 | settings: settings, 65 | onChanged: _settingsStore.updateSettings, 66 | ), 67 | Container(height: 48.0), 68 | SettingsFooterSection( 69 | appInfo: appInfo, 70 | onPrivacyPolicyPressed: () => launchUrlString(_settingsStore.privacyPolicyUrl), 71 | onShowIntroPressed: () => PingerApp.router.show(RouteConfig.intro()), 72 | ), 73 | ], 74 | ), 75 | ); 76 | }), 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /mobile/lib/ui/shared/chart/result_summary_chart.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_chart/fl_chart.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:pinger/extensions.dart'; 4 | import 'package:pinger/resources.dart'; 5 | 6 | class ResultSummaryChart extends StatelessWidget { 7 | const ResultSummaryChart({ 8 | super.key, 9 | required this.minIndex, 10 | required this.maxIndex, 11 | required this.values, 12 | }); 13 | 14 | final int minIndex; 15 | final int maxIndex; 16 | final List values; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return LineChart(LineChartData( 21 | titlesData: const FlTitlesData(show: false), 22 | borderData: FlBorderData(show: false), 23 | gridData: const FlGridData(show: false), 24 | lineTouchData: const LineTouchData(enabled: false), 25 | lineBarsData: [ 26 | LineChartBarData( 27 | isStrokeCapRound: true, 28 | dotData: FlDotData( 29 | show: true, 30 | getDotPainter: (_, __, ___, ____) => FlDotCirclePainter( 31 | color: R.colors.secondary, 32 | strokeWidth: 0.0, 33 | ), 34 | checkToShowDot: (it, _) => it.x == minIndex || it.x == maxIndex, 35 | ), 36 | isCurved: true, 37 | preventCurveOverShooting: true, 38 | belowBarData: BarAreaData( 39 | show: true, 40 | gradient: LinearGradient( 41 | colors: [ 42 | R.colors.primaryLight.withOpacity(0.7), 43 | R.colors.primaryLight.withOpacity(0.2), 44 | R.colors.primaryLight.withOpacity(0.0), 45 | ], 46 | stops: const [0.0, 0.7, 1.0], 47 | begin: Alignment.topCenter, 48 | end: Alignment.bottomCenter, 49 | ), 50 | ), 51 | color: R.colors.primaryLight, 52 | spots: values 53 | .mapIndexed( 54 | (i, e) => e != null ? FlSpot(i.toDouble(), e.toDouble()) : null, 55 | ) 56 | .whereNotNull() 57 | .toList(), 58 | ), 59 | // Add invisible line to prevent chart from being cut if there are values on the edges 60 | LineChartBarData( 61 | color: R.colors.none, 62 | spots: values 63 | .mapIndexed((i, _) => FlSpot(i.toDouble(), values[minIndex]!.toDouble())) 64 | .toList(), 65 | ), 66 | ], 67 | )); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /mobile/lib/ui/shared/chart/result_tile_chart.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_chart/fl_chart.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:pinger/extensions.dart'; 4 | import 'package:pinger/resources.dart'; 5 | 6 | class ResultTileChart extends StatelessWidget { 7 | const ResultTileChart({ 8 | super.key, 9 | required this.values, 10 | required this.min, 11 | required this.mean, 12 | required this.max, 13 | required this.barWidth, 14 | }); 15 | 16 | final Radius barRadius = const Radius.circular(2.0); 17 | 18 | final List values; 19 | final int min; 20 | final int mean; 21 | final int max; 22 | final double barWidth; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Stack(children: [ 27 | BarChart(BarChartData( 28 | groupsSpace: 0.0, 29 | borderData: FlBorderData(show: false), 30 | titlesData: const FlTitlesData(show: false), 31 | barTouchData: BarTouchData(enabled: false), 32 | gridData: const FlGridData(show: false), 33 | barGroups: [ 34 | _buildBarData(0, min, barWidth), 35 | _buildBarData(1, mean, barWidth), 36 | _buildBarData(2, max, barWidth), 37 | ], 38 | )), 39 | LineChart(LineChartData( 40 | titlesData: const FlTitlesData(show: false), 41 | borderData: FlBorderData(show: false), 42 | gridData: const FlGridData(show: false), 43 | lineTouchData: const LineTouchData(enabled: false), 44 | lineBarsData: [ 45 | LineChartBarData( 46 | dotData: const FlDotData(show: false), 47 | isCurved: true, 48 | preventCurveOverShooting: true, 49 | color: R.colors.secondary, 50 | barWidth: 1.0, 51 | spots: values 52 | .mapIndexed( 53 | (i, e) => e != null ? FlSpot(i.toDouble(), e.toDouble()) : null, 54 | ) 55 | .whereNotNull() 56 | .toList(), 57 | ), 58 | ], 59 | )), 60 | ]); 61 | } 62 | 63 | BarChartGroupData _buildBarData(int index, int value, double width) { 64 | return BarChartGroupData(x: index, barRods: [ 65 | BarChartRodData( 66 | toY: value.toDouble(), 67 | width: width, 68 | color: R.colors.primaryLight.withOpacity(0.5), 69 | borderRadius: BorderRadius.only( 70 | topLeft: barRadius, 71 | topRight: barRadius, 72 | bottomLeft: index == 0 ? barRadius : Radius.zero, 73 | bottomRight: index == 2 ? barRadius : Radius.zero, 74 | ), 75 | ), 76 | ]); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /mobile/lib/ui/shared/info_section.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class InfoSection extends StatelessWidget { 6 | const InfoSection({ 7 | super.key, 8 | required this.image, 9 | required this.title, 10 | required this.description, 11 | this.risksOverflow = false, 12 | }); 13 | 14 | final double size = 144.0; 15 | 16 | final AssetImage image; 17 | final String title; 18 | final String description; 19 | final bool risksOverflow; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Column( 24 | mainAxisAlignment: MainAxisAlignment.center, 25 | children: [ 26 | _buildImage(), 27 | Container(height: 36.0), 28 | Text( 29 | title, 30 | style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold), 31 | textAlign: TextAlign.center, 32 | ), 33 | Container(height: 12.0), 34 | Text( 35 | description, 36 | style: const TextStyle(fontSize: 18.0), 37 | textAlign: TextAlign.center, 38 | ), 39 | ], 40 | ); 41 | } 42 | 43 | Widget _buildImage() { 44 | return risksOverflow 45 | ? Flexible( 46 | child: LayoutBuilder( 47 | builder: (_, constraints) => SizedBox( 48 | height: min(constraints.maxHeight, size), 49 | width: size, 50 | child: Image(image: image), 51 | ), 52 | ), 53 | ) 54 | : Image(image: image, width: size, height: size); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /mobile/lib/ui/shared/sheet/replace_session_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:pinger/generated/l10n.dart'; 4 | import 'package:pinger/resources.dart'; 5 | import 'package:pinger/ui/shared/sheet/pinger_bottom_sheet.dart'; 6 | 7 | class ReplaceSessionSheet { 8 | static Future show({ 9 | required BuildContext context, 10 | required String currentHost, 11 | required String newHost, 12 | required VoidCallback onAcceptPressed, 13 | }) async { 14 | final subStyle = R.styles.bottomSheetSubtitle; 15 | final subPaths = S.current 16 | .replaceSessionSheetSubtitle(currentHost, newHost) 17 | .split(RegExp("($currentHost|$newHost)")); 18 | await PingerBottomSheet.show( 19 | title: Text( 20 | S.current.replaceSessionSheetTitle, 21 | style: R.styles.bottomSheetTitle, 22 | ), 23 | subtitle: RichText( 24 | text: TextSpan(children: [ 25 | TextSpan( 26 | text: subPaths[0], 27 | style: subStyle, 28 | ), 29 | TextSpan( 30 | text: currentHost, 31 | style: subStyle.copyWith(color: R.colors.primaryLight), 32 | ), 33 | TextSpan(text: subPaths[1], style: subStyle), 34 | TextSpan( 35 | text: newHost, 36 | style: subStyle.copyWith(color: R.colors.primaryLight), 37 | ), 38 | TextSpan(text: subPaths[2], style: subStyle), 39 | ]), 40 | ), 41 | rejectLabel: S.current.cancelButtonLabel, 42 | onAcceptPressed: onAcceptPressed, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /mobile/lib/ui/shared/three_bounce.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class ThreeBounce extends StatefulWidget { 6 | const ThreeBounce({ 7 | super.key, 8 | this.color, 9 | this.size = 48.0, 10 | this.duration = const Duration(milliseconds: 1500), 11 | }); 12 | 13 | final Color? color; 14 | final double size; 15 | final Duration duration; 16 | 17 | @override 18 | State createState() => _ThreeBounceState(); 19 | } 20 | 21 | class _ThreeBounceState extends State with SingleTickerProviderStateMixin { 22 | late AnimationController _controller; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _controller = AnimationController( 28 | vsync: this, 29 | duration: widget.duration, 30 | )..repeat(); 31 | } 32 | 33 | @override 34 | void dispose() { 35 | _controller.dispose(); 36 | super.dispose(); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return Center( 42 | child: SizedBox.fromSize( 43 | size: Size(widget.size * 2, widget.size), 44 | child: Row( 45 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 46 | children: List.generate(3, _buildDot), 47 | ), 48 | ), 49 | ); 50 | } 51 | 52 | Widget _buildDot(index) { 53 | return ScaleTransition( 54 | scale: DelayTween( 55 | begin: 0.0, 56 | end: 1.0, 57 | delay: index * 0.2, 58 | ).animate(_controller), 59 | child: SizedBox.fromSize( 60 | size: Size.square(widget.size * 0.5), 61 | child: DecoratedBox( 62 | decoration: BoxDecoration( 63 | color: widget.color, 64 | shape: BoxShape.circle, 65 | ), 66 | ), 67 | ), 68 | ); 69 | } 70 | } 71 | 72 | class DelayTween extends Tween { 73 | DelayTween({ 74 | required double begin, 75 | required double end, 76 | required this.delay, 77 | }) : super(begin: begin, end: end); 78 | 79 | final double? delay; 80 | 81 | @override 82 | double lerp(double t) => super.lerp((sin((t - delay!) * 2 * pi) + 1) / 2); 83 | 84 | @override 85 | double evaluate(Animation animation) => lerp(animation.value); 86 | } 87 | -------------------------------------------------------------------------------- /mobile/lib/ui/shared/tiles/host_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/resources.dart'; 3 | import 'package:pinger/ui/shared/tiles/host_icon_tile.dart'; 4 | 5 | enum HostTileType { 6 | regular, 7 | highlighted, 8 | removable, 9 | removableSelected, 10 | } 11 | 12 | class HostTile extends StatelessWidget { 13 | const HostTile({ 14 | super.key, 15 | required this.host, 16 | this.loadIcon = true, 17 | this.onPressed, 18 | this.onLongPress, 19 | this.trailing, 20 | this.type = HostTileType.regular, 21 | }); 22 | 23 | final String? host; 24 | final bool loadIcon; 25 | final VoidCallback? onPressed; 26 | final VoidCallback? onLongPress; 27 | final Widget? trailing; 28 | final HostTileType type; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return ElevatedButton( 33 | style: _getElevatedButtonStyle(), 34 | key: ValueKey(host), 35 | onPressed: onPressed, 36 | onLongPress: onLongPress, 37 | child: Row(children: [ 38 | HostIconTile(host: loadIcon ? host : null), 39 | Expanded( 40 | child: Padding( 41 | padding: const EdgeInsets.only(left: 18.0, right: 12.0), 42 | child: Text( 43 | host!, 44 | maxLines: 1, 45 | softWrap: false, 46 | overflow: TextOverflow.fade, 47 | style: TextStyle( 48 | color: type == HostTileType.highlighted ? R.colors.white : R.colors.primary, 49 | ), 50 | ), 51 | ), 52 | ), 53 | if (trailing != null) trailing!, 54 | Padding( 55 | padding: const EdgeInsets.symmetric(horizontal: 12.0), 56 | child: Icon( 57 | type == HostTileType.removableSelected || type == HostTileType.removable 58 | ? Icons.delete 59 | : Icons.chevron_right, 60 | color: type == HostTileType.removableSelected 61 | ? R.colors.secondary 62 | : type == HostTileType.highlighted 63 | ? R.colors.secondary 64 | : R.colors.gray, 65 | ), 66 | ), 67 | ]), 68 | ); 69 | } 70 | 71 | ButtonStyle _getElevatedButtonStyle() => ElevatedButton.styleFrom( 72 | padding: const EdgeInsets.only(left: 12.0), 73 | backgroundColor: type == HostTileType.highlighted 74 | ? R.colors.primaryLight 75 | : type == HostTileType.removableSelected 76 | ? R.colors.secondary.withOpacity(0.5) 77 | : R.colors.grayLight, 78 | ).copyWith(elevation: MaterialStateProperty.resolveWith( 79 | (states) { 80 | if (type != HostTileType.highlighted) return 0.0; 81 | 82 | if (states.contains(MaterialState.pressed)) return 8.0; 83 | 84 | return 0.0; 85 | }, 86 | )); 87 | } 88 | -------------------------------------------------------------------------------- /mobile/lib/ui/shared/tiles/results_group_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/generated/l10n.dart'; 3 | import 'package:pinger/resources.dart'; 4 | import 'package:pinger/ui/shared/tiles/host_icon_tile.dart'; 5 | 6 | class ResultsGroupTile extends StatelessWidget { 7 | const ResultsGroupTile({ 8 | super.key, 9 | required this.host, 10 | required this.resultsCount, 11 | this.onPressed, 12 | }); 13 | 14 | final String host; 15 | final int resultsCount; 16 | final VoidCallback? onPressed; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return OutlinedButton( 21 | key: ValueKey(host), 22 | style: OutlinedButton.styleFrom( 23 | padding: const EdgeInsets.symmetric(horizontal: 12.0), 24 | side: R.styles.outlineButtonBorder, 25 | ), 26 | onPressed: onPressed, 27 | child: Column( 28 | mainAxisSize: MainAxisSize.min, 29 | children: [ 30 | const Spacer(), 31 | HostIconTile(host: host), 32 | const Spacer(), 33 | Text( 34 | host, 35 | maxLines: 1, 36 | softWrap: false, 37 | overflow: TextOverflow.fade, 38 | ), 39 | Container(height: 12.0), 40 | Text( 41 | S.current.resultsGroupCount(resultsCount), 42 | style: TextStyle(fontSize: 12.0, color: R.colors.gray), 43 | ), 44 | const Spacer(), 45 | ], 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /mobile/lib/ui/shared/view_type/view_type_button.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:pinger/resources.dart'; 5 | 6 | class ViewTypeButton extends StatelessWidget { 7 | const ViewTypeButton({ 8 | super.key, 9 | required this.label, 10 | required this.selected, 11 | required this.onPressed, 12 | }); 13 | 14 | static const height = 26.0; 15 | 16 | final String? label; 17 | final bool selected; 18 | final VoidCallback onPressed; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final radius = BorderRadius.circular(20.0); 23 | final color = selected ? R.colors.accent : R.colors.gray; 24 | return LayoutBuilder( 25 | builder: (_, constraints) => Container( 26 | height: height, 27 | constraints: BoxConstraints(maxWidth: min(56.0, constraints.maxWidth)), 28 | decoration: BoxDecoration( 29 | border: Border.all(color: color), 30 | borderRadius: radius, 31 | color: selected ? color.withOpacity(0.33) : R.colors.none, 32 | ), 33 | child: InkResponse( 34 | splashColor: R.colors.none, 35 | highlightShape: BoxShape.rectangle, 36 | borderRadius: radius, 37 | highlightColor: color.withOpacity(0.33), 38 | onTap: onPressed, 39 | child: Center( 40 | child: Text( 41 | label!, 42 | style: TextStyle( 43 | fontSize: 12.0, 44 | color: selected ? R.colors.accent : R.colors.gray, 45 | ), 46 | ), 47 | ), 48 | ), 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /mobile/lib/ui/shared/view_type/view_type_row.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pinger/ui/shared/view_type/view_type_button.dart'; 3 | 4 | class ViewTypeRow extends StatelessWidget { 5 | const ViewTypeRow({ 6 | super.key, 7 | required this.labeledTypes, 8 | required this.selection, 9 | required this.onChanged, 10 | }); 11 | 12 | final Map labeledTypes; 13 | final T selection; 14 | final ValueChanged onChanged; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return LayoutBuilder( 19 | builder: (_, constraints) { 20 | final buttonWidth = constraints.maxWidth / labeledTypes.length; 21 | return Row( 22 | mainAxisAlignment: MainAxisAlignment.end, 23 | children: 24 | labeledTypes.entries.map((it) => _buildViewTypeButton(it.key, buttonWidth)).toList(), 25 | ); 26 | }, 27 | ); 28 | } 29 | 30 | Widget _buildViewTypeButton(T type, double maxWidth) { 31 | return ConstrainedBox( 32 | constraints: BoxConstraints(maxWidth: maxWidth), 33 | child: Padding( 34 | padding: const EdgeInsets.only(left: 8.0), 35 | child: ViewTypeButton( 36 | label: labeledTypes[type], 37 | selected: selection == type, 38 | onPressed: () { 39 | if (selection != type) onChanged(type); 40 | }, 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /mobile/lib/ui/shared/view_type/view_types.dart: -------------------------------------------------------------------------------- 1 | enum PingValuesType { gauge, list, chart } 2 | 3 | enum UserResultType { min, mean, max } 4 | -------------------------------------------------------------------------------- /mobile/lib/utils/data_snap.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'data_snap.freezed.dart'; 4 | 5 | @freezed 6 | class DataSnap with _$DataSnap { 7 | const factory DataSnap.data(T value) = SnapData; 8 | const factory DataSnap.loading() = SnapLoading; 9 | const factory DataSnap.error() = SnapError; 10 | } 11 | -------------------------------------------------------------------------------- /mobile/lib/utils/format_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | import 'package:pinger/generated/l10n.dart'; 4 | import 'package:pinger/model/user_settings.dart'; 5 | import 'package:pinger/resources.dart'; 6 | 7 | class FormatUtils { 8 | static String getSinceNowLabel(DateTime timestamp) { 9 | final diff = DateTime.now().difference(timestamp); 10 | if (diff.inDays >= 7) { 11 | return S.current.weeksSinceNow(diff.inDays ~/ 7); 12 | } else if (diff.inHours >= 24) { 13 | return S.current.daysSinceNow(diff.inHours ~/ 24); 14 | } else if (diff.inMinutes >= 60) { 15 | return S.current.hoursSinceNow(diff.inMinutes ~/ 60); 16 | } else if (diff.inSeconds >= 60) { 17 | return S.current.minutesSinceNow(diff.inSeconds ~/ 60); 18 | } else { 19 | return S.current.secondsSinceNow(diff.inSeconds); 20 | } 21 | } 22 | 23 | static String getDurationLabel(Duration duration) { 24 | final min = duration.inMinutes; 25 | final sec = duration.inSeconds - min * 60; 26 | return "$min:${sec.toString().padLeft(2, '0')}"; 27 | } 28 | 29 | static String getTimestampLabel(DateTime timestamp, {bool showTime = false}) { 30 | var formatter = DateFormat.MMMd(); 31 | if (showTime) formatter = formatter.add_Hm(); 32 | return formatter.format(timestamp); 33 | } 34 | 35 | static String getCountLabel(NumSetting setting) { 36 | return setting.when( 37 | finite: (it) => it.toString(), 38 | infinite: () => R.symbols.infinity, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /mobile/lib/utils/host_tap_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:pinger/model/ping_session.dart'; 4 | import 'package:pinger/store/ping_store.dart'; 5 | import 'package:pinger/ui/app/pinger_app.dart'; 6 | import 'package:pinger/ui/app/pinger_router.dart'; 7 | import 'package:pinger/ui/shared/sheet/replace_session_sheet.dart'; 8 | 9 | mixin HostTapHandler on State { 10 | void onHostTap(PingStore pingStore, String newHost) { 11 | final status = pingStore.currentSession?.status; 12 | final host = pingStore.currentSession?.host; 13 | if (host == newHost) { 14 | _showPingPage(); 15 | } else if (status.isNull || status.isInitial || status.isSessionDone) { 16 | pingStore.initSession(newHost); 17 | _showPingPage(); 18 | } else { 19 | ReplaceSessionSheet.show( 20 | context: context, 21 | currentHost: host!, 22 | newHost: newHost, 23 | onAcceptPressed: () { 24 | pingStore.initSession(newHost); 25 | _showPingPage(); 26 | }, 27 | ); 28 | } 29 | } 30 | 31 | void _showPingPage() => PingerApp.router.show(RouteConfig.session()); 32 | } 33 | -------------------------------------------------------------------------------- /mobile/lib/utils/lifecycle_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | @injectable 5 | class LifecycleNotifier extends WidgetsBindingObserver { 6 | LifecycleNotifier() { 7 | WidgetsBinding.instance.addObserver(this); 8 | } 9 | 10 | final Set _listeners = {}; 11 | 12 | void register(LifecycleAware listener) => _listeners.add(listener); 13 | 14 | void unregister(LifecycleAware listener) => _listeners.remove(listener); 15 | 16 | @override 17 | void didChangeAppLifecycleState(AppLifecycleState state) { 18 | switch (state) { 19 | case AppLifecycleState.paused: 20 | for (var it in _listeners) { 21 | it.onPaused(); 22 | } 23 | break; 24 | case AppLifecycleState.resumed: 25 | for (var it in _listeners) { 26 | it.onResumed(); 27 | } 28 | break; 29 | case AppLifecycleState.detached: 30 | for (var it in _listeners) { 31 | it.onDetached(); 32 | } 33 | break; 34 | case AppLifecycleState.inactive: 35 | for (var it in _listeners) { 36 | it.onInactive(); 37 | } 38 | break; 39 | case AppLifecycleState.hidden: 40 | for (var it in _listeners) { 41 | it.onHidden(); 42 | } 43 | break; 44 | } 45 | } 46 | } 47 | 48 | mixin class LifecycleAware { 49 | factory LifecycleAware({ 50 | VoidCallback? onPaused, 51 | VoidCallback? onResumed, 52 | VoidCallback? onInactive, 53 | VoidCallback? onDetached, 54 | VoidCallback? onHidden, 55 | }) => 56 | _FunLifecycleAware( 57 | onPaused, 58 | onResumed, 59 | onInactive, 60 | onDetached, 61 | onHidden, 62 | ); 63 | 64 | void onPaused() {} 65 | void onResumed() {} 66 | void onInactive() {} 67 | void onDetached() {} 68 | void onHidden() {} 69 | } 70 | 71 | class _FunLifecycleAware implements LifecycleAware { 72 | _FunLifecycleAware( 73 | this._onPaused, 74 | this._onResumed, 75 | this._onInactive, 76 | this._onDetached, 77 | this._onHidden, 78 | ); 79 | 80 | final VoidCallback? _onPaused; 81 | final VoidCallback? _onResumed; 82 | final VoidCallback? _onInactive; 83 | final VoidCallback? _onDetached; 84 | final VoidCallback? _onHidden; 85 | 86 | @override 87 | void onPaused() => _onPaused?.call(); 88 | 89 | @override 90 | void onResumed() => _onResumed?.call(); 91 | 92 | @override 93 | void onInactive() => _onInactive?.call(); 94 | 95 | @override 96 | void onDetached() => _onDetached?.call(); 97 | 98 | @override 99 | void onHidden() => _onHidden?.call(); 100 | } 101 | -------------------------------------------------------------------------------- /mobile/lib/utils/notification_messages.dart: -------------------------------------------------------------------------------- 1 | import 'package:pinger/generated/l10n.dart'; 2 | 3 | class NotificationMessages { 4 | String startedTitle(String host) => S.current.notificationStartedTitle(host); 5 | 6 | String startedBody(int last) => S.current.notificationStartedBody(last); 7 | 8 | String pausedTitle(String host) => S.current.notificationPausedTitle(host); 9 | 10 | String pausedBody(int length, String count) => S.current.notificationPausedBody(length, count); 11 | 12 | String doneTitle(String host) => S.current.notificationDoneTitle(host); 13 | 14 | String doneBody(int min, int mean, int max) => S.current.notificationDoneBody(min, mean, max); 15 | } 16 | -------------------------------------------------------------------------------- /mobile/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: pinger 2 | description: Ping command utility app made with Flutter. 3 | 4 | version: 1.1.7+12 5 | 6 | environment: 7 | sdk: ">=3.1.1 <4.0.0" 8 | 9 | dependencies: 10 | app_settings: ^5.1.1 11 | cloud_firestore: ^4.13.6 12 | connectivity: ^3.0.6 13 | dotted_border: ^2.1.0 14 | firebase_core: ^2.24.2 15 | firebase_crashlytics: ^3.4.8 16 | fl_chart: ^0.65.0 17 | flutter: 18 | sdk: flutter 19 | flutter_local_notifications: ^16.2.0 20 | flutter_localizations: 21 | sdk: flutter 22 | flutter_mobx: ^2.2.0+1 23 | flutter_vibrate: ^1.3.0 24 | freezed_annotation: ^2.4.1 25 | get_it: ^7.6.4 26 | google_fonts: ^6.1.0 27 | http: ^1.1.2 28 | injectable: ^2.3.2 29 | intl: ^0.18.1 30 | json_annotation: ^4.8.1 31 | location: ^5.0.3 32 | mobx: ^2.2.3 33 | package_info: ^2.0.2 34 | path_provider: ^2.1.1 35 | permission_handler: ^11.1.0 36 | shared_preferences: ^2.2.2 37 | url_launcher: ^6.2.2 38 | 39 | dev_dependencies: 40 | build_runner: ^2.4.7 41 | flutter_lints: ^3.0.1 42 | flutter_test: 43 | sdk: flutter 44 | freezed: ^2.4.5 45 | import_sorter: ^4.6.0 46 | intl_utils: ^2.8.6 47 | injectable_generator: ^2.4.1 48 | json_serializable: ^6.7.1 49 | mobx_codegen: ^2.4.0 50 | mockito: ^5.4.3 51 | r_flutter: ^0.9.0 52 | 53 | flutter: 54 | uses-material-design: true 55 | assets: 56 | - lib/assets/images/ 57 | - lib/assets/icons/ 58 | 59 | flutter_intl: 60 | enabled: true 61 | -------------------------------------------------------------------------------- /mobile/r_flutter.build.yaml: -------------------------------------------------------------------------------- 1 | builders: 2 | r_flutter: 3 | import: "package:r_flutter/builder.dart" 4 | builder_factories: 5 | - "builder" 6 | build_extensions: {"$lib$": ["assets.dart"]} 7 | auto_apply: root_package 8 | build_to: source -------------------------------------------------------------------------------- /mobile/test/extensions_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:pinger/extensions.dart'; 3 | 4 | void main() { 5 | group("IterableExtensions", () { 6 | test("mapIndexed iterates over list with current index", () { 7 | final list = [8, 2, 9, 5, 1, 7, 0]; 8 | 9 | final matchesInput = list.mapIndexed((i, e) => list[i] == e).every((it) => it); 10 | 11 | expect(matchesInput, true); 12 | 13 | int? lastIndex; 14 | final iteratesInOrder = list.mapIndexed((i, _) { 15 | final delta = i - (lastIndex ?? -1); 16 | lastIndex = i; 17 | return delta; 18 | }).every((it) => it == 1); 19 | 20 | expect(iteratesInOrder, true); 21 | }); 22 | }); 23 | 24 | group('NullableIterableExtensions', () { 25 | test("isNullOrEmpty returns true only if list exists and has element", () { 26 | List? list = [true, 2, "item"]; 27 | 28 | expect(list.isNullOrEmpty, false); 29 | 30 | list.clear(); 31 | 32 | expect(list.isNullOrEmpty, true); 33 | 34 | list = null; 35 | 36 | expect(list.isNullOrEmpty, true); 37 | }); 38 | 39 | test("lastOrNull returns null if list is empty", () { 40 | final list = []; 41 | 42 | expect(list.lastOrNull, null); 43 | }); 44 | 45 | test("lastOrNull returns last element if list is not empty", () { 46 | final List list = [4]; 47 | 48 | expect(list.lastOrNull, 4); 49 | 50 | list.add(null); 51 | 52 | expect(list.lastOrNull, null); 53 | }); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /mobile/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | pinger 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------