├── 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 | [
](https://apps.apple.com/app/id1520063947)
6 | [
](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 | /// 
5 | static AssetImage get appIconDev => const AssetImage("lib/assets/icons/app-icon-dev.png");
6 | /// 
7 | static AssetImage get appIconProd => const AssetImage("lib/assets/icons/app-icon-prod.png");
8 | /// 
9 | static AssetImage get splashHd => const AssetImage("lib/assets/images/splash-hd.png");
10 | /// 
11 | static AssetImage get splash => const AssetImage("lib/assets/images/splash.png");
12 | /// 
13 | static AssetImage get undrawCollecting => const AssetImage("lib/assets/images/undraw_collecting.png");
14 | /// 
15 | static AssetImage get undrawEmpty => const AssetImage("lib/assets/images/undraw_empty.png");
16 | /// 
17 | static AssetImage get undrawRoadSign => const AssetImage("lib/assets/images/undraw_road_sign.png");
18 | /// 
19 | static AssetImage get undrawRunnerStart => const AssetImage("lib/assets/images/undraw_runner_start.png");
20 | /// 
21 | static AssetImage get undrawSearching => const AssetImage("lib/assets/images/undraw_searching.png");
22 | /// 
23 | static AssetImage get undrawServerDown => const AssetImage("lib/assets/images/undraw_server_down.png");
24 | /// 
25 | static AssetImage get undrawSettings => const AssetImage("lib/assets/images/undraw_settings.png");
26 | /// 
27 | static AssetImage get undrawSignalSearching => const AssetImage("lib/assets/images/undraw_signal_searching.png");
28 | /// 
29 | static AssetImage get undrawTheWorldIsMine => const AssetImage("lib/assets/images/undraw_the_world_is_mine.png");
30 | /// 
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 |
--------------------------------------------------------------------------------