├── android ├── keystore.properties ├── app │ ├── proguard-rules.pro │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable │ │ │ │ │ ├── msm.png │ │ │ │ │ ├── background.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-hdpi │ │ │ │ │ ├── splash.png │ │ │ │ │ ├── android12splash.png │ │ │ │ │ └── ic_bg_service_small.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ ├── splash.png │ │ │ │ │ ├── android12splash.png │ │ │ │ │ └── ic_bg_service_small.png │ │ │ │ ├── drawable-v21 │ │ │ │ │ ├── background.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ ├── splash.png │ │ │ │ │ ├── android12splash.png │ │ │ │ │ └── ic_bg_service_small.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ ├── splash.png │ │ │ │ │ ├── android12splash.png │ │ │ │ │ └── ic_bg_service_small.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ ├── splash.png │ │ │ │ │ └── android12splash.png │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── drawable-night-hdpi │ │ │ │ │ └── android12splash.png │ │ │ │ ├── drawable-night-mdpi │ │ │ │ │ └── android12splash.png │ │ │ │ ├── drawable-night-xhdpi │ │ │ │ │ └── android12splash.png │ │ │ │ ├── drawable-night-xxhdpi │ │ │ │ │ └── android12splash.png │ │ │ │ ├── drawable-night-xxxhdpi │ │ │ │ │ └── android12splash.png │ │ │ │ ├── values-v31 │ │ │ │ │ └── styles.xml │ │ │ │ ├── values-night-v31 │ │ │ │ │ └── styles.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── msm │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle └── settings.gradle ├── .releaserc ├── assets ├── images │ ├── splash.png │ └── app_icon.png └── svgs │ └── msm.svg ├── fastlane └── metadata │ └── android │ ├── de │ └── short_description.txt │ └── en-US │ ├── short_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 01_home.jpeg │ │ ├── 04_settings.jpeg │ │ ├── 06_services.jpeg │ │ ├── 07_upload.jpeg │ │ ├── 09_file_list.jpeg │ │ ├── 03_configuration.jpeg │ │ ├── 08_system_tools.jpeg │ │ ├── 02_server_details.jpeg │ │ └── 05_server_functions.jpeg │ └── full_description.txt ├── msm_splash.yaml ├── devtools_options.yaml ├── linux_update_commands.json ├── msm_icons.yaml ├── analysis_options.yaml ├── lib ├── ui_components │ ├── textfield │ │ ├── textfield_decoration.dart │ │ ├── validators.dart │ │ ├── input_formatters.dart │ │ └── textfield.dart │ ├── text │ │ ├── text.dart │ │ └── textstyles.dart │ ├── switch │ │ └── switch.dart │ ├── loading │ │ └── loading_overlay.dart │ └── floating_action_button │ │ └── fab.dart ├── views │ ├── splash │ │ └── init_screen.dart │ ├── home │ │ ├── home_utils.dart │ │ ├── real_time_basic_details.dart │ │ ├── home.dart │ │ └── home_common_widgets.dart │ ├── upload_pages │ │ ├── upload_menu.dart │ │ ├── upload_item_card.dart │ │ └── common_upload_interface.dart │ ├── notifications │ │ └── notifications.dart │ ├── settings │ │ ├── folder_configuration_view.dart │ │ ├── settings.dart │ │ ├── server_functions_view.dart │ │ ├── server_details │ │ │ └── ssh_settings.dart │ │ └── app_info.dart │ ├── system_tools │ │ ├── system_tools.dart │ │ ├── live_terminal.dart │ │ └── system_tool_utils.dart │ └── file_listing │ │ └── file_tile.dart ├── utils │ ├── server_functions.dart │ ├── file_upload.dart │ ├── commands │ │ ├── services.dart │ │ └── commands.dart │ ├── server_details.dart │ ├── folder_configuration.dart │ ├── send_to_kindle.dart │ ├── ssh_keypair.dart │ ├── storage.dart │ ├── server.dart │ ├── background_tasks.dart │ ├── local_notification.dart │ └── upload_and_download.dart ├── generated_plugin_registrant.dart ├── providers │ ├── folder_configuration_provider.dart │ ├── file_listing_provider.dart │ ├── app_provider.dart │ └── upload_provider.dart ├── constants │ ├── colors.dart │ └── constants.dart ├── main.dart ├── config.dart ├── utils.dart └── router │ ├── router_utils.dart │ └── router.dart ├── dart_dependency_validator.yaml ├── .pre-commit-config.yaml ├── .github ├── dependabot.yml └── workflows │ ├── checks.yml │ ├── commit_changelog_and_version.yml │ └── build_and_release.yml ├── _typos.toml ├── test └── widget_test.dart ├── update_version.sh ├── pubspec.yaml ├── CONTRIBUTING.md ├── .gitignore ├── README.md ├── cliff.toml └── shell_scripts └── basic_details.sh /android/keystore.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "new_Design", 4 | "master" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/assets/images/splash.png -------------------------------------------------------------------------------- /fastlane/metadata/android/de/short_description.txt: -------------------------------------------------------------------------------- 1 | All-in-One Manager für Define Media-Server 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | All in one manager for your media server 2 | -------------------------------------------------------------------------------- /assets/images/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/assets/images/app_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/msm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable/msm.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/mipmap-hdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/mipmap-mdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /msm_splash.yaml: -------------------------------------------------------------------------------- 1 | flutter_native_splash: 2 | color: "#000000" 3 | image: assets/images/app_icon.png 4 | android_12: 5 | image: assets/images/splash.png 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-hdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-mdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-xhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_bg_service_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-hdpi/ic_bg_service_small.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_bg_service_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-mdpi/ic_bg_service_small.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-xxhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-xxxhdpi/android12splash.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-hdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-night-hdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-mdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-night-mdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_bg_service_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-xhdpi/ic_bg_service_small.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_bg_service_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_bg_service_small.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-xhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-night-xhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-xxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/01_home.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/01_home.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/04_settings.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/04_settings.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/06_services.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/06_services.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/07_upload.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/07_upload.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/09_file_list.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/09_file_list.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/03_configuration.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/03_configuration.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/08_system_tools.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/08_system_tools.jpeg -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/msm/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.prinzpiuz.msm 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/02_server_details.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/02_server_details.jpeg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/05_server_functions.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prinzpiuz/MSM/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/05_server_functions.jpeg -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /linux_update_commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "Debian": { 3 | "update": "sudo apt update", 4 | "list": "sudo apt list --upgradable", 5 | "upgrade": "sudo apt upgrade -y", 6 | "after": "sudo apt autoremove" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /msm_icons.yaml: -------------------------------------------------------------------------------- 1 | dev_dependencies: 2 | flutter_launcher_icons: "^0.11.0" 3 | 4 | flutter_icons: 5 | android: "launcher_icon" 6 | ios: true 7 | adaptive_icon_background: "#000000" 8 | image_path: "assets/images/app_icon.png" 9 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: [build/**] 5 | language: 6 | 7 | linter: 8 | rules: 9 | always_declare_return_types: true 10 | avoid_print: true 11 | await_only_futures: true 12 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/ui_components/textfield/textfield_decoration.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | class AppTextFieldDecoratoion { 5 | static InputDecoration simpleTextFieldDecoration() { 6 | return const InputDecoration( 7 | border: InputBorder.none, 8 | hintText: 'Search...', 9 | ); 10 | } 11 | 12 | AppTextFieldDecoratoion._(); 13 | } 14 | -------------------------------------------------------------------------------- /lib/views/splash/init_screen.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Project imports: 5 | import 'package:msm/common_widgets.dart'; 6 | 7 | class InitScreen extends StatelessWidget { 8 | const InitScreen({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return commonCircularProgressIndicator; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /dart_dependency_validator.yaml: -------------------------------------------------------------------------------- 1 | # dart_dependency_validator.yaml 2 | 3 | # Set true if you allow pinned packages in your project. 4 | allow_pins: true 5 | # Exclude one or more paths from being scanned. Supports glob syntax. 6 | exclude: 7 | - "app/**" 8 | # Ignore one or more packages. 9 | ignore: 10 | - flutter_background_service_android 11 | - flutter_web_plugins 12 | - shared_preferences_web 13 | - flutter_launcher_icons 14 | - flutter_native_splash 15 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - id: check-merge-conflict 9 | - id: check-added-large-files 10 | - id: check-merge-conflict 11 | - id: check-case-conflict 12 | - id: check-json 13 | - repo: https://github.com/crate-ci/typos 14 | rev: v1.22.0 15 | hooks: 16 | - id: typos 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pub" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /lib/utils/server_functions.dart: -------------------------------------------------------------------------------- 1 | class ServerFunctionsData { 2 | bool wakeOnLan = false; 3 | bool autoUpdate = false; 4 | bool sendTokindle = false; 5 | 6 | ServerFunctionsData(); 7 | 8 | ServerFunctionsData.fromJson(Map json) 9 | : wakeOnLan = json['wakeOnLan'], 10 | autoUpdate = json['autoUpdate'], 11 | sendTokindle = json['sendTokindle']; 12 | 13 | Map toJson() => { 14 | 'wakeOnLan': wakeOnLan, 15 | 'autoUpdate': autoUpdate, 16 | 'sendTokindle': sendTokindle 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /lib/generated_plugin_registrant.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // ignore_for_file: directives_ordering 6 | // ignore_for_file: lines_longer_than_80_chars 7 | // ignore_for_file: depend_on_referenced_packages 8 | 9 | import 'package:shared_preferences_web/shared_preferences_web.dart'; 10 | 11 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 12 | 13 | // ignore: public_member_api_docs 14 | void registerPlugins(Registrar registrar) { 15 | SharedPreferencesPlugin.registerWith(registrar); 16 | registrar.registerMessageHandler(); 17 | } 18 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = ["CHANGELOG.md", "ios/Runner/Base.lproj/Main.storyboard", "ios/*", "android/*"] 3 | ignore-hidden = true 4 | ignore-files = true 5 | ignore-dot = true 6 | ignore-vcs = true 7 | ignore-global = true 8 | ignore-parent = true 9 | 10 | [default] 11 | binary = false 12 | check-filename = true 13 | check-file = true 14 | unicode = true 15 | ignore-hex = true 16 | identifier-leading-digits = false 17 | locale = "en" 18 | extend-ignore-identifiers-re = [] 19 | extend-ignore-words-re = [] 20 | extend-ignore-re = [] 21 | 22 | [default.extend-identifiers] 23 | 24 | [default.extend-words] 25 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 |

MSM works as wrapper around your Media server(emby, jellyfin, kodi, plex) and helps you to manage your media files, like CRUD operations also helps to manage server services without touching server. All you need is android mobile phone and media server which are connected to same network.


Features:

2 | -------------------------------------------------------------------------------- /lib/utils/file_upload.dart: -------------------------------------------------------------------------------- 1 | // Project imports: 2 | import 'package:msm/utils/file_manager.dart'; 3 | 4 | class FileUploadData { 5 | List uploadData = []; 6 | FileUploadData(); 7 | 8 | void get clear => uploadData.clear(); 9 | 10 | void addOrRemove(FileOrDirectory data) { 11 | if (uploadData.contains(data)) { 12 | uploadData.remove(data); 13 | } else { 14 | uploadData.add(data); 15 | } 16 | } 17 | 18 | List get localFilesPaths { 19 | List paths = []; 20 | for (FileOrDirectory file in uploadData) { 21 | paths.add(file.fullPath); 22 | } 23 | return paths; 24 | } 25 | 26 | bool fileAdded(FileOrDirectory data) => uploadData.contains(data); 27 | } 28 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | // Package imports: 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | // Project imports: 12 | import 'package:msm/main.dart'; 13 | 14 | void main() { 15 | testWidgets('MSM UI testing', (WidgetTester tester) async { 16 | // Build our app and trigger a frame. 17 | await tester.pumpWidget(const MSM()); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /lib/providers/folder_configuration_provider.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | class FolderConfigState with ChangeNotifier { 5 | bool addNewPath = false; 6 | int foldersCount = 0; 7 | List pathTextFields = []; 8 | FolderConfigState(); 9 | 10 | void resetFolderCount() => foldersCount = 0; 11 | 12 | void removeFromWidgetList(int index) { 13 | pathTextFields.removeAt(index); 14 | foldersCount--; 15 | notifyListeners(); 16 | } 17 | 18 | void incrementFolderCount() { 19 | foldersCount++; 20 | notifyListeners(); 21 | } 22 | 23 | void decrementFolderCount() { 24 | foldersCount--; 25 | notifyListeners(); 26 | } 27 | 28 | set setAddNewPath(bool addpath) { 29 | addNewPath = true; 30 | notifyListeners(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.9.1" apply false 22 | id "org.jetbrains.kotlin.android" version "2.1.0" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /update_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit immediately if a command exits with a non-zero status. 4 | 5 | PUBSPEC_FILE="pubspec.yaml" 6 | NEW_SEMANTIC_VERSION="$1" 7 | 8 | # Get the current version 9 | CURRENT_VERSION=$(grep "^version:" "$PUBSPEC_FILE" | awk '{print $2}') 10 | 11 | # echo "Current version: $CURRENT_VERSION" 12 | 13 | # Extract the build number from the current version 14 | CURRENT_BUILD_NUMBER=$(echo "$CURRENT_VERSION" | cut -d'+' -f2) 15 | 16 | # Increment the build number 17 | NEW_BUILD_NUMBER=$((CURRENT_BUILD_NUMBER+=1)) 18 | # CURRENT_BUILD_NUMBER++ 19 | 20 | # Construct the new version string 21 | NEW_VERSION="${NEW_SEMANTIC_VERSION}+${NEW_BUILD_NUMBER}" 22 | 23 | # Replace the version in pubspec.yaml 24 | sed -i "s/^version: .*/version: ${NEW_VERSION}/" "$PUBSPEC_FILE" 25 | 26 | echo "Updated version in $PUBSPEC_FILE to: $NEW_VERSION" 27 | -------------------------------------------------------------------------------- /lib/views/home/home_utils.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:go_router/go_router.dart'; 6 | 7 | // Project imports: 8 | import 'package:msm/router/router_utils.dart'; 9 | 10 | void goToPage(int index, BuildContext context) { 11 | switch (index) { 12 | case 0: 13 | return GoRouter.of(context).go(Pages.upload.toPath); 14 | case 1: 15 | return GoRouter.of(context).go(Pages.systemTools.toPath); 16 | case 2: 17 | return GoRouter.of(context).go(Pages.fileList.toPath); 18 | case 3: 19 | return GoRouter.of(context).go(Pages.settings.toPath); 20 | } 21 | } 22 | 23 | void notificationsPage(BuildContext context, DragUpdateDetails details) { 24 | int sensitivity = 8; 25 | if (details.delta.dx > sensitivity) { 26 | GoRouter.of(context).go(Pages.notifications.toPath); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/constants/colors.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart' show Color, Colors; 3 | 4 | class CommonColors { 5 | static const commonGreenColor = Color.fromRGBO(9, 135, 6, 1); 6 | static const diskUsageBackgroundColor = Color.fromRGBO(175, 185, 175, 1); 7 | static const commonBlackColor = Colors.black; 8 | static const commonWhiteColor = Colors.white; 9 | static const commonGreyColor = Color.fromRGBO(63, 63, 63, 1); 10 | static const commonLinkColor = Color.fromRGBO(88, 166, 255, 1); 11 | } 12 | 13 | ///Text Form Field 14 | class TextFormColors { 15 | static const inputHintTextColor = Color(0xFF838383); 16 | static const inputHelperTextColor = Color(0xFF838383); 17 | static const inputErrorTextColor = Colors.red; 18 | static const inputTextColor = Colors.black; 19 | static const passwordIconColor = Colors.black; 20 | static const fillColor = Colors.white; 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | name: Pre-Merging checks 4 | 5 | env: 6 | FLUTTER_VERSION: "3.38.3" 7 | 8 | jobs: 9 | pre-commit: 10 | name: Run Pre-commit 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-python@v3 15 | - uses: subosito/flutter-action@v2 16 | - uses: pre-commit/action@v3.0.0 17 | sort_analyse_test: 18 | name: Sort Analyze Test 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: subosito/flutter-action@v2 23 | with: 24 | flutter-version: ${{ env.FLUTTER_VERSION }} 25 | channel: "stable" 26 | - run: flutter --version 27 | - run: flutter pub get 28 | - run: flutter pub run import_sorter:main 29 | - run: flutter pub run dependency_validator 30 | - run: flutter analyze 31 | - run: flutter test 32 | -------------------------------------------------------------------------------- /lib/views/upload_pages/upload_menu.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Project imports: 5 | import 'package:msm/utils.dart'; 6 | import 'package:msm/common_widgets.dart'; 7 | import 'package:msm/constants/colors.dart'; 8 | import 'package:msm/router/router_utils.dart'; 9 | import 'package:msm/views/upload_pages/upload_page_utils.dart'; 10 | 11 | class UploadMenuPage extends StatefulWidget { 12 | const UploadMenuPage({super.key}); 13 | 14 | @override 15 | UploadMenuPageState createState() => UploadMenuPageState(); 16 | } 17 | 18 | class UploadMenuPageState extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | return handleBackButton( 22 | child: uploadMenu(context), 23 | context: context, 24 | backRoute: Pages.home.toPath); 25 | } 26 | } 27 | 28 | Widget uploadMenu(BuildContext context) { 29 | return Scaffold( 30 | appBar: commonAppBar( 31 | text: Pages.upload.toTitle, 32 | backroute: Pages.home.toPath, 33 | context: context), 34 | backgroundColor: CommonColors.commonWhiteColor, 35 | body: ListView( 36 | children: locations(context), 37 | )); 38 | } 39 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/widgets.dart'; 3 | 4 | // Project imports: 5 | import 'package:msm/initialization.dart'; 6 | import 'package:msm/views/splash/init_screen.dart'; 7 | 8 | import 'config.dart'; 9 | 10 | void main() { 11 | runApp(const MSM()); 12 | } 13 | 14 | class MSM extends StatefulWidget { 15 | const MSM({super.key}); 16 | 17 | @override 18 | State createState() => _MSMState(); 19 | } 20 | 21 | class _MSMState extends State { 22 | final Future _initApp = Init().initialize(); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return FutureBuilder( 27 | future: _initApp, 28 | builder: (context, snapshot) { 29 | if (snapshot.connectionState == ConnectionState.done && 30 | snapshot.hasData) { 31 | var data = snapshot.data as Map; 32 | return materialApp( 33 | appService: data["appService"], 34 | uploadService: data["uploadService"], 35 | fileListingService: data["fileListingService"], 36 | folderConfigState: data["folderConfigState"]); 37 | } else { 38 | return const InitScreen(); 39 | } 40 | }, 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/ui_components/textfield/validators.dart: -------------------------------------------------------------------------------- 1 | // Project imports: 2 | import 'package:msm/constants/constants.dart'; 3 | 4 | String? validateServerName(String? value) { 5 | if (value!.isEmpty) { 6 | return "Name can't be empty"; 7 | } 8 | if (value.length > 15) { 9 | return "Name should be less 15 chars"; 10 | } 11 | return null; 12 | } 13 | 14 | String? validatePortNumber(String? value) { 15 | if (value!.isEmpty) { 16 | return "Port can't be empty"; 17 | } 18 | if (value.contains(RegExp(AppConstants.alphaAndSpecialChars))) { 19 | return "Only numbers can be used"; 20 | } 21 | if (value.length > 5 || int.parse(value) > 65535) { 22 | return "Length should be < 5 and Value should less than 65535"; 23 | } 24 | return null; 25 | } 26 | 27 | String? valueNeeded(String? value) { 28 | if (value!.isEmpty) { 29 | return "Value can't be empty"; 30 | } 31 | return null; 32 | } 33 | 34 | String? macValidation(String? value) { 35 | if (value!.length < 17 && value.length > 1) { 36 | return "MAC address length will be 17 including :"; 37 | } 38 | return null; 39 | } 40 | 41 | String? validateEmail(String? value) { 42 | if (RegExp(AppConstants.emailvalidationRegex).hasMatch(value!)) { 43 | return null; 44 | } 45 | return "Invalid Email Address"; 46 | } 47 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: msm 2 | description: Media Server Manager. 3 | 4 | publish_to: "none" 5 | 6 | version: 1.11.0+20 7 | 8 | environment: 9 | sdk: ">=3.0.0 <4.0.0" 10 | flutter: ^3.38.3 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | 16 | shared_preferences: ^2.1.0 17 | dartssh2: ^2.13.0 18 | permission_handler: ^12.0.1 19 | filesize: ^2.0.1 20 | flutter_screenutil: ^5.9.3 21 | font_awesome_flutter: ^10.12.0 22 | google_fonts: ^6.3.2 23 | go_router: ^17.0.0 24 | provider: ^6.1.5+1 25 | package_info_plus: ^9.0.0 26 | url_launcher: ^6.3.2 27 | flutter_svg: ^2.2.3 28 | flutter_local_notifications: ^19.5.0 29 | xterm: ^4.0.0 30 | flutter_background_service: ^5.0.2 31 | wake_on_lan: ^4.1.1+3 32 | flutter_email_sender: ^8.0.0 33 | path_provider: ^2.1.5 34 | file_picker: ^10.3.7 35 | pointycastle: ^3.9.1 36 | basic_utils: ^5.7.0 37 | 38 | dev_dependencies: 39 | flutter_test: 40 | sdk: flutter 41 | 42 | flutter_lints: ^6.0.0 43 | import_sorter: ^4.6.0 44 | flutter_launcher_icons: ^0.14.4 45 | dependency_validator: ^5.0.3 46 | flutter_native_splash: ^2.4.7 47 | 48 | flutter: 49 | uses-material-design: true 50 | assets: 51 | - assets/images/splash.png 52 | - assets/svgs/msm.svg 53 | - assets/images/app_icon.png 54 | - shell_scripts/basic_details.sh 55 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /lib/views/home/real_time_basic_details.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Project imports: 5 | import 'package:msm/common_widgets.dart'; 6 | import 'package:msm/providers/app_provider.dart'; 7 | import 'package:msm/utils/commands/basic_details.dart'; 8 | import 'package:msm/views/home/home_common_widgets.dart'; 9 | 10 | class RealTimeBasicDetails extends StatefulWidget { 11 | final AppService appService; 12 | final BasicDetails basicDetails; 13 | const RealTimeBasicDetails( 14 | {super.key, required this.appService, required this.basicDetails}); 15 | 16 | @override 17 | RealTimeBasicDetailsState createState() => RealTimeBasicDetailsState(); 18 | } 19 | 20 | class RealTimeBasicDetailsState extends State { 21 | @override 22 | void dispose() { 23 | super.dispose(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return StreamBuilder( 29 | initialData: widget.basicDetails, 30 | stream: fetchBasicDetailsLive(widget.appService), 31 | builder: ( 32 | BuildContext context, 33 | AsyncSnapshot snapshot, 34 | ) { 35 | if (snapshot.hasData) { 36 | return serverDetails(snapshot.data); 37 | } 38 | return commonCircularProgressIndicator; 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/ui_components/text/text.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart' show Text, TextAlign, TextOverflow; 3 | import 'package:flutter/material.dart' show TextStyle; 4 | 5 | class AppText { 6 | static Text singleLineText(String text, 7 | {TextAlign textAlign = TextAlign.left, 8 | TextStyle? style, 9 | TextOverflow overflow = TextOverflow.ellipsis}) { 10 | return Text( 11 | text, 12 | textAlign: textAlign, 13 | maxLines: 1, 14 | style: style, 15 | overflow: overflow, 16 | ); 17 | } 18 | 19 | static Text centerSingleLineText(String text, 20 | {TextStyle? style, TextOverflow overflow = TextOverflow.ellipsis}) { 21 | return singleLineText(text, 22 | textAlign: TextAlign.center, style: style, overflow: overflow); 23 | } 24 | 25 | static Text text(String str, 26 | {TextAlign textAlign = TextAlign.left, 27 | int? maxLines, 28 | TextStyle? style, 29 | TextOverflow overflow = TextOverflow.visible}) { 30 | return Text( 31 | str, 32 | overflow: overflow, 33 | textAlign: textAlign, 34 | maxLines: maxLines, 35 | style: style, 36 | ); 37 | } 38 | 39 | static Text centerText(String str, 40 | {int? maxLines, 41 | TextStyle? style, 42 | TextOverflow overflow = TextOverflow.visible}) { 43 | return text(str, 44 | textAlign: TextAlign.center, 45 | maxLines: maxLines, 46 | style: style, 47 | overflow: overflow); 48 | } 49 | 50 | AppText._(); 51 | } 52 | -------------------------------------------------------------------------------- /lib/views/notifications/notifications.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 6 | 7 | // Project imports: 8 | import 'package:msm/utils.dart'; 9 | import 'package:msm/common_widgets.dart'; 10 | import 'package:msm/constants/colors.dart'; 11 | import 'package:msm/constants/constants.dart'; 12 | import 'package:msm/utils/background_tasks.dart'; 13 | import 'package:msm/router/router_utils.dart'; 14 | 15 | class NotificationsPage extends StatelessWidget { 16 | const NotificationsPage({super.key}); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return handleBackButton(context: context, child: notifications(context)); 21 | } 22 | } 23 | 24 | Widget notifications(BuildContext context) { 25 | return Scaffold( 26 | appBar: commonAppBar( 27 | text: Pages.notifications.toTitle, 28 | backroute: Pages.home.toPath, 29 | actions: [ 30 | IconButton( 31 | color: CommonColors.commonBlackColor, 32 | onPressed: () { 33 | showMessage( 34 | context: context, 35 | text: AppMessages.clearingTasks, 36 | duration: 5); 37 | BackgroundTasks.cancel(); 38 | }, 39 | icon: const Icon(FontAwesomeIcons.broom)) 40 | ], 41 | context: context), 42 | backgroundColor: CommonColors.commonWhiteColor, 43 | body: const Center( 44 | child: Text("Notifications"), 45 | )); 46 | } 47 | -------------------------------------------------------------------------------- /lib/utils/commands/services.dart: -------------------------------------------------------------------------------- 1 | // Package imports: 2 | import 'package:dartssh2/dartssh2.dart'; 3 | 4 | // Project imports: 5 | import 'package:msm/utils.dart'; 6 | import 'package:msm/utils/commands/commands.dart'; 7 | 8 | class Services { 9 | String unit = ""; 10 | String serviceStatus = ""; 11 | String description = ""; 12 | late SSHClient client; 13 | 14 | bool get isActive { 15 | if (serviceStatus == "running") { 16 | return true; 17 | } 18 | return false; 19 | } 20 | 21 | Services( 22 | {required this.unit, 23 | required this.serviceStatus, 24 | required this.description}); 25 | 26 | String get serviceName => unit.split(".").first; 27 | 28 | Future get start async { 29 | try { 30 | String command = 31 | CommandBuilder.addArguments(Commands.serviceStart, [unit]); 32 | await client.run(command); 33 | return true; 34 | } catch (_) { 35 | return false; 36 | } 37 | } 38 | 39 | Future get stop async { 40 | try { 41 | String command = 42 | CommandBuilder.addArguments(Commands.serviceStop, [unit]); 43 | await client.run(command); 44 | return true; 45 | } catch (_) { 46 | return false; 47 | } 48 | } 49 | 50 | Future get restart async { 51 | try { 52 | String command = 53 | CommandBuilder.addArguments(Commands.serviceRestart, [unit]); 54 | await client.run(command); 55 | return true; 56 | } catch (_) { 57 | return false; 58 | } 59 | } 60 | 61 | Future get status async { 62 | try { 63 | String command = 64 | CommandBuilder.addArguments(Commands.serviceStatus, [unit]); 65 | return decodeOutput(await client.run(command)); 66 | } catch (e) { 67 | return e.toString(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/commit_changelog_and_version.yml: -------------------------------------------------------------------------------- 1 | name: Commit Changelog And Version 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | concurrency: production 8 | jobs: 9 | tag: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: "0" 15 | - name: Bump version and push tag 16 | id: autoversion 17 | uses: ietf-tools/semver-action@v1 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | branch: main 21 | skipInvalidTags: true 22 | noVersionBumpBehavior: patch 23 | outputs: 24 | new_tag: ${{ steps.autoversion.outputs.nextStrict }} 25 | commit_changelog_and_version: 26 | needs: tag 27 | runs-on: ubuntu-latest 28 | name: Generate changelog and update version 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | - name: Generate a changelog 35 | uses: orhun/git-cliff-action@main 36 | id: git-cliff 37 | with: 38 | config: cliff.toml 39 | args: --sort newest --tag ${{ needs.tag.outputs.new_tag }} 40 | env: 41 | OUTPUT: CHANGES.md 42 | - name: Copy changelog to workspace 43 | run: echo "${{ steps.git-cliff.outputs.content }}" > CHANGELOG.md 44 | - name: Edit pubspec version 45 | run: | 46 | ./update_version.sh ${{ needs.tag.outputs.new_tag }} 47 | - name: Commit changes 48 | if: ${{ success() }} 49 | uses: EndBug/add-and-commit@v9 50 | with: 51 | add: '["CHANGELOG.md", "pubspec.yaml"]' 52 | pull: "--rebase --autostash ." 53 | message: "chore(generated): changelog generated & version updated automatically" 54 | default_author: github_actions 55 | -------------------------------------------------------------------------------- /lib/views/upload_pages/upload_item_card.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | 7 | // Project imports: 8 | import 'package:msm/constants/colors.dart'; 9 | import 'package:msm/utils/file_manager.dart'; 10 | import 'package:msm/utils/file_upload.dart'; 11 | 12 | class UploadItemCard extends StatefulWidget { 13 | final FileOrDirectory data; 14 | final FileUploadData fileUploadData; 15 | const UploadItemCard( 16 | {super.key, required this.data, required this.fileUploadData}); 17 | 18 | @override 19 | UploadItemCardState createState() => UploadItemCardState(); 20 | } 21 | 22 | class UploadItemCardState extends State { 23 | bool selected = false; 24 | @override 25 | Widget build(BuildContext context) { 26 | return Positioned( 27 | bottom: 15.h, 28 | right: 50.w, 29 | child: widget.data.isFile 30 | ? Container( 31 | width: 60.w, 32 | height: 60.h, 33 | decoration: const BoxDecoration( 34 | shape: BoxShape.circle, color: CommonColors.commonGreenColor), 35 | child: IconButton( 36 | onPressed: () { 37 | setState(() { 38 | selected = !selected; 39 | widget.fileUploadData.addOrRemove(widget.data); 40 | }); 41 | }, 42 | icon: Icon( 43 | selected ? Icons.remove : Icons.add, 44 | color: CommonColors.commonWhiteColor, 45 | size: 30.h, 46 | )), 47 | ) 48 | : Icon( 49 | Icons.folder, 50 | color: CommonColors.commonGreyColor, 51 | size: 40.h, 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/utils/server_details.dart: -------------------------------------------------------------------------------- 1 | // Dart imports: 2 | import 'dart:io'; 3 | 4 | import 'package:dartssh2/dartssh2.dart' show SSHKeyPair; 5 | 6 | class ServerData { 7 | String serverName; 8 | String serverHost; 9 | String username; 10 | String rootPassword; 11 | String portNumber; 12 | String macAddress; 13 | String privateKeyPath; 14 | List? cachedPrivateKey; 15 | 16 | ServerData( 17 | {this.serverName = "", 18 | this.serverHost = "", 19 | this.username = "", 20 | this.rootPassword = "", 21 | this.portNumber = "22", 22 | this.macAddress = "", 23 | this.privateKeyPath = ""}); 24 | 25 | int get port => int.parse(portNumber); 26 | 27 | bool get detailsAvailable { 28 | if (serverHost.isNotEmpty && 29 | username.isNotEmpty && 30 | (rootPassword.isNotEmpty || privateKeyPath.isNotEmpty) && 31 | portNumber.isNotEmpty) { 32 | return true; 33 | } 34 | return false; 35 | } 36 | 37 | bool get detailsForKeyUploadAvailable { 38 | if (serverHost.isNotEmpty && username.isNotEmpty && portNumber.isNotEmpty) { 39 | return true; 40 | } 41 | return false; 42 | } 43 | 44 | InternetAddress get host => InternetAddress(serverHost); 45 | 46 | ServerData.fromJson(Map json) 47 | : serverName = json['serverName'], 48 | serverHost = json['serverHost'], 49 | username = json['username'], 50 | rootPassword = json['rootPassword'], 51 | portNumber = json['portNumber'], 52 | macAddress = json['macAddress'], 53 | privateKeyPath = json['privateKeyPath']; 54 | 55 | Map toJson() => { 56 | 'serverName': serverName, 57 | 'serverHost': serverHost, 58 | 'username': username, 59 | 'rootPassword': rootPassword, 60 | 'portNumber': portNumber, 61 | 'macAddress': macAddress, 62 | 'privateKeyPath': privateKeyPath 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /lib/ui_components/switch/switch.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | 7 | // Project imports: 8 | import 'package:msm/constants/colors.dart'; 9 | import 'package:msm/constants/constants.dart'; 10 | import 'package:msm/ui_components/text/text.dart'; 11 | import 'package:msm/ui_components/text/textstyles.dart'; 12 | 13 | class CommonSwitch extends StatefulWidget { 14 | final String text; 15 | final bool value; 16 | final ValueChanged onChanged; 17 | const CommonSwitch( 18 | {super.key, 19 | required this.text, 20 | required this.onChanged, 21 | required this.value}); 22 | 23 | @override 24 | State createState() => _CommonSwitchState(); 25 | } 26 | 27 | class _CommonSwitchState extends State { 28 | bool initial = true; 29 | late bool switchValue; 30 | @override 31 | Widget build(BuildContext context) { 32 | if (initial) { 33 | switchValue = widget.value; 34 | initial = false; 35 | } 36 | return Padding( 37 | padding: EdgeInsets.only(left: 18.w, right: 18.w), 38 | child: Row( 39 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 40 | children: [ 41 | AppText.singleLineText(widget.text, 42 | style: AppTextStyles.medium(CommonColors.commonBlackColor, 43 | AppFontSizes.systemToolsTittleFontSize.sp)), 44 | Switch( 45 | value: switchValue, 46 | trackColor: const WidgetStatePropertyAll(Colors.grey), 47 | thumbColor: const WidgetStatePropertyAll(Colors.black), 48 | onChanged: (value) { 49 | setState(() { 50 | switchValue = value; 51 | widget.onChanged(value); 52 | }); 53 | }, 54 | ) 55 | ], 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/ui_components/loading/loading_overlay.dart: -------------------------------------------------------------------------------- 1 | //code took from here 2 | //https://dartling.dev/displaying-a-loading-overlay-or-progress-hud-in-flutter 3 | 4 | // Dart imports: 5 | import 'dart:ui'; 6 | 7 | // Flutter imports: 8 | import 'package:flutter/material.dart'; 9 | 10 | // Project imports: 11 | import 'package:msm/common_widgets.dart'; 12 | 13 | class LoadingOverlay extends StatefulWidget { 14 | const LoadingOverlay({ 15 | super.key, 16 | required this.child, 17 | this.delay = const Duration(milliseconds: 500), 18 | }); 19 | 20 | final Widget child; 21 | final Duration delay; 22 | 23 | static LoadingOverlayState of(BuildContext context) { 24 | return context.findAncestorStateOfType()!; 25 | } 26 | 27 | @override 28 | State createState() => LoadingOverlayState(); 29 | } 30 | 31 | class LoadingOverlayState extends State { 32 | bool _isLoading = false; 33 | 34 | void show() { 35 | setState(() { 36 | _isLoading = true; 37 | }); 38 | } 39 | 40 | void hide() { 41 | setState(() { 42 | _isLoading = false; 43 | }); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return Stack( 49 | children: [ 50 | widget.child, 51 | if (_isLoading) 52 | BackdropFilter( 53 | filter: ImageFilter.blur(sigmaX: 4.0, sigmaY: 4.0), 54 | child: const Opacity( 55 | opacity: 0.3, 56 | child: ModalBarrier(dismissible: false, color: Colors.black), 57 | ), 58 | ), 59 | if (_isLoading) 60 | Center( 61 | child: FutureBuilder( 62 | future: Future.delayed(widget.delay), 63 | builder: (context, snapshot) { 64 | return snapshot.connectionState == ConnectionState.done 65 | ? commonCircularProgressIndicator 66 | : const SizedBox(); 67 | }, 68 | ), 69 | ), 70 | ], 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/config.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | // Project imports: 10 | import 'package:msm/providers/app_provider.dart'; 11 | import 'package:msm/providers/file_listing_provider.dart'; 12 | import 'package:msm/providers/folder_configuration_provider.dart'; 13 | import 'package:msm/providers/upload_provider.dart'; 14 | import 'package:msm/router/router.dart'; 15 | 16 | MultiProvider materialApp( 17 | {required AppService appService, 18 | required UploadState uploadService, 19 | required FileListingState fileListingService, 20 | required FolderConfigState folderConfigState}) { 21 | return MultiProvider( 22 | providers: [ 23 | ChangeNotifierProvider(create: (_) => appService), 24 | ChangeNotifierProvider(create: (_) => uploadService), 25 | ChangeNotifierProvider( 26 | create: (_) => fileListingService), 27 | ChangeNotifierProvider( 28 | create: (_) => folderConfigState), 29 | Provider(create: (_) => AppRouter(appService)), 30 | ], 31 | child: Builder( 32 | builder: (context) { 33 | final GoRouter goRouter = 34 | Provider.of(context, listen: false).router; 35 | return ScreenUtilInit( 36 | builder: (_, child) { 37 | return MaterialApp.router( 38 | title: "MSM", 39 | routeInformationProvider: goRouter.routeInformationProvider, 40 | routeInformationParser: goRouter.routeInformationParser, 41 | routerDelegate: goRouter.routerDelegate, 42 | theme: ThemeData( 43 | useMaterial3: false, 44 | textTheme: 45 | Typography.englishLike2018.apply(fontSizeFactor: 1.sp), 46 | ), 47 | ); 48 | }, 49 | ); 50 | }, 51 | ), 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /lib/utils/folder_configuration.dart: -------------------------------------------------------------------------------- 1 | // Project imports: 2 | import 'package:msm/views/upload_pages/upload_page_utils.dart'; 3 | 4 | class FolderConfiguration { 5 | String movies; 6 | String tv; 7 | String books; 8 | List customFolders; 9 | 10 | FolderConfiguration( 11 | {this.movies = "", 12 | this.tv = "", 13 | this.books = "", 14 | this.customFolders = const []}); 15 | 16 | void addExtraFolder(String path) { 17 | if (!customFolders.contains(path)) { 18 | customFolders.add(path); 19 | } 20 | } 21 | 22 | void removeExtraFolder(int index) { 23 | customFolders.removeAt(index); 24 | } 25 | 26 | String? pathToDirectory(UploadCatogories catogories) { 27 | switch (catogories) { 28 | case UploadCatogories.movies: 29 | return movies; 30 | case UploadCatogories.tvShows: 31 | return tv; 32 | case UploadCatogories.books: 33 | return books; 34 | case UploadCatogories.custom: 35 | return "custom"; 36 | } 37 | } 38 | 39 | bool get dataAvailable { 40 | if (movies.isNotEmpty || 41 | tv.isNotEmpty || 42 | books.isNotEmpty || 43 | customFolders.isNotEmpty) { 44 | return true; 45 | } 46 | return false; 47 | } 48 | 49 | List get allPaths { 50 | List allFolders = []; 51 | if (movies.isNotEmpty) { 52 | allFolders.add(movies); 53 | } 54 | if (tv.isNotEmpty) { 55 | allFolders.add(tv); 56 | } 57 | if (books.isNotEmpty) { 58 | allFolders.add(books); 59 | } 60 | if (customFolders.isNotEmpty) { 61 | allFolders.addAll(customFolders); 62 | } 63 | return allFolders; 64 | } 65 | 66 | FolderConfiguration.fromJson(Map json) 67 | : movies = json['movies'], 68 | tv = json['tv'], 69 | books = json['books'], 70 | customFolders = json['customFolders'].cast(); 71 | 72 | Map toJson() => { 73 | 'movies': movies, 74 | 'tv': tv, 75 | 'books': books, 76 | 'customFolders': customFolders 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /lib/views/settings/folder_configuration_view.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:provider/provider.dart'; 6 | 7 | // Project imports: 8 | import 'package:msm/common_widgets.dart'; 9 | import 'package:msm/constants/colors.dart'; 10 | import 'package:msm/utils/folder_configuration.dart'; 11 | import 'package:msm/providers/app_provider.dart'; 12 | import 'package:msm/providers/folder_configuration_provider.dart'; 13 | import 'package:msm/router/router_utils.dart'; 14 | import 'package:msm/views/settings/settings_utils.dart'; 15 | 16 | class FolderConfigurationForm extends StatefulWidget { 17 | const FolderConfigurationForm({super.key}); 18 | 19 | @override 20 | State createState() => 21 | _FolderConfigurationFormState(); 22 | } 23 | 24 | class _FolderConfigurationFormState extends State { 25 | @override 26 | Widget build(BuildContext context) { 27 | return folderConfigurationForm(context); 28 | } 29 | } 30 | 31 | Widget folderConfigurationForm(BuildContext context) { 32 | final formKey = GlobalKey(); 33 | FolderConfiguration folderConfiguration = 34 | Provider.of(context).storage.getFolderConfigurations; 35 | getFoldersList(context, folderConfiguration, formKey); 36 | List folders = Provider.of(context).pathTextFields; 37 | return Scaffold( 38 | appBar: commonAppBar( 39 | backroute: Pages.settings.toPath, 40 | context: context, 41 | text: SettingsSubRoute.folderConfiguration.toTitle), 42 | backgroundColor: CommonColors.commonWhiteColor, 43 | floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, 44 | floatingActionButton: saveButton( 45 | onPressed: () => 46 | saveFolderConfigurations(formKey, folderConfiguration, context), 47 | ), 48 | body: Form( 49 | key: formKey, 50 | child: ListView.builder( 51 | itemCount: folders.length, 52 | itemBuilder: (BuildContext context, int index) { 53 | return folders[index]; 54 | }), 55 | )); 56 | } 57 | -------------------------------------------------------------------------------- /lib/views/settings/settings.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | 8 | // Project imports: 9 | import 'package:msm/utils.dart'; 10 | import 'package:msm/common_widgets.dart'; 11 | import 'package:msm/constants/colors.dart'; 12 | import 'package:msm/router/router_utils.dart'; 13 | 14 | class Settings extends StatelessWidget { 15 | const Settings({super.key}); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return handleBackButton(context: context, child: settingsMenu(context)); 20 | } 21 | } 22 | 23 | Widget settingsMenu(BuildContext context) { 24 | return Scaffold( 25 | appBar: commonAppBar( 26 | text: Pages.settings.toTitle, 27 | backroute: Pages.home.toPath, 28 | context: context), 29 | backgroundColor: CommonColors.commonWhiteColor, 30 | body: ListView( 31 | padding: commonListViewTopPadding, 32 | children: [ 33 | commonTile( 34 | icon: FontAwesomeIcons.computer, 35 | title: 'Server Details', 36 | subtitle: 'Details Regarding Server', 37 | onTap: () { 38 | context.goNamed(SettingsSubRoute.serverDetails.toName); 39 | }), 40 | commonTile( 41 | icon: FontAwesomeIcons.folderClosed, 42 | title: 'Folder Configurations', 43 | subtitle: 'Folder Locations In Server', 44 | onTap: () { 45 | context.goNamed(SettingsSubRoute.folderConfiguration.toName); 46 | }), 47 | commonTile( 48 | icon: FontAwesomeIcons.gears, 49 | title: 'Server Functions', 50 | subtitle: 'Extra Functions In Server', 51 | onTap: () { 52 | context.goNamed(SettingsSubRoute.serverFunctions.toName); 53 | }), 54 | commonTile( 55 | icon: FontAwesomeIcons.circleInfo, 56 | title: 'App Info', 57 | subtitle: 'Application Details', 58 | onTap: () { 59 | context.goNamed(SettingsSubRoute.appInfo.toName); 60 | }), 61 | ], 62 | )); 63 | } 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Thank you for considering contributing to this project! 🎉 4 | We welcome contributions of all kinds — bug fixes, new features, documentation improvements, or even suggestions. 5 | 6 | Please join this [Telegram](https://t.me/joinchat/FDVzK06Rt7vsNQLBLi2icw) group for further discussion 7 | 8 | --- 9 | 10 | ## 🚀 How to Contribute 11 | 12 | 1. **Fork the repository** 13 | Create your own fork and clone it locally. 14 | 15 | 2. **Create a branch** 16 | Always create a new branch for your work. Example: 17 | ```bash 18 | git checkout -b feature/my-new-feature 19 | ``` 20 | 21 | 3. **Make your changes** 22 | - If you found a bug 🐛, try to include a test if possible. 23 | - If you’re adding a feature ✨, please describe it clearly in your PR. 24 | - Keep code clean and readable. 25 | 26 | 4. **Commit with Commitizen** 27 | We use [Commitizen](https://github.com/commitizen/cz-cli) for commit messages. 28 | To commit: 29 | ```bash 30 | git cz 31 | ``` 32 | This ensures consistent, conventional commits. 33 | 34 | 5. **Push and open a Pull Request (PR)** 35 | Push your branch and open a PR with a clear description. 36 | 37 | --- 38 | 39 | ## 📝 Guidelines 40 | 41 | - **Be respectful**: We’re building this together — kindness goes a long way. 42 | - **Keep PRs small**: It’s easier to review and merge. 43 | - **Discuss big features**: Open an issue before starting on large changes. 44 | - **Follow coding style**: Stick to existing formatting and practices. 45 | 46 | --- 47 | 48 | ## 🙌 For Beginners 49 | 50 | This is a beginner-friendly project! 51 | - Don’t worry if your first PR isn’t perfect — we’ll help you through it. 52 | - Start small: fix typos, improve docs, or add minor features. 53 | - Learn by doing: open-source is one of the best ways to practice. 54 | 55 | --- 56 | 57 | ## 🐛 Found a Bug? 58 | 59 | - Open an issue describing the bug. 60 | - Mention steps to reproduce it. 61 | - If possible, suggest a fix. 62 | 63 | --- 64 | 65 | ## 💡 Want a New Feature? 66 | 67 | - Open an issue with your idea. 68 | - Explain why it would be useful. 69 | - If you can, contribute the feature with a PR! 70 | 71 | --- 72 | 73 | ## 📜 License 74 | 75 | By contributing, you agree that your contributions will be licensed under the same license as this project. 76 | 77 | --- 78 | 79 | 👉 That’s it! Jump in, try the app, and help us improve it. 80 | -------------------------------------------------------------------------------- /lib/utils/send_to_kindle.dart: -------------------------------------------------------------------------------- 1 | // Package imports: 2 | // import 'package:dio/dio.dart'; 3 | 4 | // Dart imports: 5 | import 'dart:convert'; 6 | import 'dart:io'; 7 | import 'dart:typed_data'; 8 | 9 | // Package imports: 10 | import 'package:flutter_email_sender/flutter_email_sender.dart'; 11 | import 'package:path_provider/path_provider.dart'; 12 | 13 | // Project imports: 14 | import 'package:msm/utils/local_notification.dart'; 15 | 16 | class KindleData { 17 | String fromEmail = ""; 18 | String kindleMailAddress = ""; 19 | 20 | KindleData(); 21 | 22 | bool get dataAvailable { 23 | return fromEmail.isNotEmpty && kindleMailAddress.isNotEmpty; 24 | } 25 | 26 | KindleData.fromJson(Map json) 27 | : fromEmail = json['fromEmail'], 28 | kindleMailAddress = json['kindleMailAddress']; 29 | 30 | Map toJson() => 31 | {'fromEmail': fromEmail, 'kindleMailAddress': kindleMailAddress}; 32 | } 33 | 34 | Future _getDecodedFile(String filename) async { 35 | final tempDir = await getTemporaryDirectory(); 36 | final file = File('${tempDir.path}/$filename'); 37 | return file; 38 | } 39 | 40 | Future _decodeBase64ToFile(String base64String, String filename) async { 41 | Uint8List decodedBytes = base64Decode(base64String); 42 | File decodedFile = await _getDecodedFile(filename); 43 | await decodedFile.writeAsBytes(decodedBytes); 44 | return decodedFile.path; 45 | } 46 | 47 | class SendTokindle { 48 | final KindleData kindleData; 49 | final String fileName; 50 | bool enabled = false; 51 | String base64EncodedData = ""; 52 | late Notifications notifications; 53 | 54 | SendTokindle( 55 | {required this.base64EncodedData, 56 | required this.notifications, 57 | required this.enabled, 58 | required this.kindleData, 59 | required this.fileName}); 60 | 61 | Future sendMail() async { 62 | if (enabled) { 63 | String filePath = await _decodeBase64ToFile(base64EncodedData, fileName); 64 | final Email email = Email( 65 | subject: "Kindle Ebook $fileName", 66 | cc: [kindleData.fromEmail], 67 | recipients: [kindleData.kindleMailAddress], 68 | attachmentPaths: [filePath], 69 | isHTML: false, 70 | ); 71 | try { 72 | await FlutterEmailSender.send(email); 73 | return true; 74 | } catch (_) { 75 | return false; 76 | } 77 | } 78 | return false; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/providers/file_listing_provider.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Project imports: 5 | import 'package:msm/utils/file_manager.dart'; 6 | import 'package:msm/utils/folder_configuration.dart'; 7 | 8 | class FileListingState with ChangeNotifier { 9 | bool _searchMode = false; 10 | bool _applyFilter = false; 11 | bool _isLoading = false; 12 | String _nextPage = ""; 13 | String _searchText = ""; 14 | List pathTraversed = []; 15 | List originalList = []; 16 | List currentList = []; 17 | List selectedList = []; 18 | late FolderConfiguration folderConfiguration; 19 | late GestureDetector fabGestureDetector; 20 | late bool fabOpen; 21 | 22 | FileListingState(); 23 | 24 | bool get isInSearchMode => _searchMode; 25 | bool get filterApplied => _applyFilter; 26 | bool get isLoading => _isLoading; 27 | String get nextPage => _nextPage; 28 | String get searchText => _searchText; 29 | 30 | set setSearchMode(bool searchMode) { 31 | _searchMode = searchMode; 32 | notifyListeners(); 33 | } 34 | 35 | set setIsLoading(bool isLoading) { 36 | _isLoading = isLoading; 37 | notifyListeners(); 38 | } 39 | 40 | set applyFilter(bool applyFilter) { 41 | _applyFilter = applyFilter; 42 | notifyListeners(); 43 | } 44 | 45 | void get cancelModes => _applyFilter = _searchMode = false; 46 | 47 | void get turnOffFilter => _applyFilter = false; 48 | 49 | void selectOrRemoveItems(FileOrDirectory fileOrDirectory) { 50 | if (selectedList.contains(fileOrDirectory)) { 51 | selectedList.remove(fileOrDirectory); 52 | } else { 53 | selectedList.add(fileOrDirectory); 54 | } 55 | } 56 | 57 | void get clearSelection => {selectedList.clear(), notifyListeners()}; 58 | 59 | set setSearchText(String searchText) { 60 | _searchText = searchText; 61 | notifyListeners(); 62 | } 63 | 64 | void get clearSearchText => _searchText = ""; 65 | 66 | set setNextPage(String page) { 67 | _nextPage = page; 68 | notifyListeners(); 69 | } 70 | 71 | set addPath(String path) { 72 | pathTraversed.add(path); 73 | } 74 | 75 | void get popPath { 76 | if (pathTraversed.isNotEmpty) { 77 | pathTraversed.removeLast(); 78 | } 79 | } 80 | 81 | String get lastPage { 82 | if (pathTraversed.isNotEmpty) { 83 | return pathTraversed.last; 84 | } 85 | return ""; 86 | } 87 | 88 | bool get firstPage => pathTraversed.isEmpty; 89 | } 90 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | 8 | # Directory created by dartdoc 9 | # If you don't generate documentation locally you can remove this line. 10 | doc/api/ 11 | 12 | # Avoid committing generated Javascript files: 13 | *.dart.js 14 | *.info.json # Produced by the --dump-info flag. 15 | *.js # When generated by dart2js. Don't specify *.js if your 16 | # project includes source files written in JavaScript. 17 | *.js_ 18 | *.js.deps 19 | *.js.map 20 | 21 | # Miscellaneous 22 | *.class 23 | *.log 24 | *.pyc 25 | *.swp 26 | .DS_Store 27 | .atom/ 28 | .buildlog/ 29 | .history 30 | .svn/ 31 | 32 | # IntelliJ related 33 | *.iml 34 | *.ipr 35 | *.iws 36 | .idea/ 37 | 38 | # Visual Studio Code related 39 | .vscode/ 40 | 41 | # Flutter/Dart/Pub related 42 | **/doc/api/ 43 | .flutter-plugins 44 | .packages 45 | .pub-cache/ 46 | .pub/ 47 | /build/ 48 | 49 | # Android related 50 | **/android/**/gradle-wrapper.jar 51 | **/android/.gradle 52 | **/android/captures/ 53 | **/android/gradlew 54 | **/android/gradlew.bat 55 | **/android/local.properties 56 | **/android/**/GeneratedPluginRegistrant.java 57 | **/android/app/.cxx/* 58 | android/app/.cxx 59 | 60 | # iOS/XCode related 61 | **/ios/**/*.mode1v3 62 | **/ios/**/*.mode2v3 63 | **/ios/**/*.moved-aside 64 | **/ios/**/*.pbxuser 65 | **/ios/**/*.perspectivev3 66 | **/ios/**/*sync/ 67 | **/ios/**/.sconsign.dblite 68 | **/ios/**/.tags* 69 | **/ios/**/.vagrant/ 70 | **/ios/**/DerivedData/ 71 | **/ios/**/Icon? 72 | **/ios/**/Pods/ 73 | **/ios/**/.symlinks/ 74 | **/ios/**/profile 75 | **/ios/**/xcuserdata 76 | **/ios/.generated/ 77 | **/ios/Flutter/App.framework 78 | **/ios/Flutter/Flutter.framework 79 | **/ios/Flutter/Generated.xcconfig 80 | **/ios/Flutter/app.flx 81 | **/ios/Flutter/app.zip 82 | **/ios/Flutter/flutter_assets/ 83 | **/ios/ServiceDefinitions.json 84 | **/ios/Runner/GeneratedPluginRegistrant.* 85 | 86 | # Exceptions to above rules. 87 | !**/ios/**/default.mode1v3 88 | !**/ios/**/default.mode2v3 89 | !**/ios/**/default.pbxuser 90 | !**/ios/**/default.perspectivev3 91 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 92 | 93 | node_modules/ 94 | package-lock.json 95 | package.json 96 | 97 | .flutter-plugins-dependencies 98 | .env 99 | .flutter-plugins 100 | .gradle/ 101 | pubspec.lock 102 | flutter_export_environment.sh 103 | 104 | core 105 | 106 | 107 | ios/ 108 | linux/ 109 | macos/ 110 | 111 | 112 | *.jks 113 | android/keystore.properties1 114 | -------------------------------------------------------------------------------- /lib/providers/app_provider.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:flutter_background_service/flutter_background_service.dart'; 6 | 7 | // Project imports: 8 | import 'package:msm/initialization.dart'; 9 | import 'package:msm/providers/file_listing_provider.dart' show FileListingState; 10 | import 'package:msm/utils/commands/command_executer.dart'; 11 | import 'package:msm/utils/folder_configuration.dart'; 12 | import 'package:msm/utils/local_notification.dart'; 13 | import 'package:msm/utils/send_to_kindle.dart'; 14 | import 'package:msm/utils/server.dart'; 15 | import 'package:msm/utils/server_details.dart'; 16 | import 'package:msm/utils/server_functions.dart'; 17 | import 'package:msm/utils/storage.dart'; 18 | 19 | class AppService with ChangeNotifier { 20 | bool _connectionState = false; 21 | bool _initialized = false; 22 | Storage storage; 23 | Server server; 24 | FlutterBackgroundService backgroundService; 25 | KindleData kindleData = KindleData(); 26 | late CommandExecuter commandExecuter; 27 | late Notifications notifications; 28 | late FileListingState fileListingService; 29 | 30 | AppService( 31 | {required this.storage, 32 | required this.server, 33 | required this.backgroundService}); 34 | 35 | bool get connectionState => 36 | _connectionState && server.state == ServerState.connected; 37 | bool get initialized => _initialized; 38 | 39 | void get turnOffSendToKindle { 40 | server.serverFunctionsData.sendTokindle = false; 41 | notifyListeners(); 42 | } 43 | 44 | void get turnOnSendToKindle { 45 | server.serverFunctionsData.sendTokindle = true; 46 | notifyListeners(); 47 | } 48 | 49 | void get pageRefresh { 50 | notifyListeners(); 51 | } 52 | 53 | set setServer(Server server) { 54 | server = server; 55 | } 56 | 57 | set updateServerDetails(ServerData serverData) { 58 | server.serverData = serverData; 59 | Init.makeConnections(this); 60 | } 61 | 62 | set updateFolderConfigurations(FolderConfiguration folderConfiguration) { 63 | server.folderConfiguration = folderConfiguration; 64 | commandExecuter.folderConfiguration = folderConfiguration; 65 | fileListingService.folderConfiguration = folderConfiguration; 66 | } 67 | 68 | set updateServerFunctions(ServerFunctionsData serverFunctionsData) { 69 | server.serverFunctionsData = serverFunctionsData; 70 | commandExecuter.serverFunctionsData = serverFunctionsData; 71 | } 72 | 73 | set connectionState(bool state) { 74 | _connectionState = state; 75 | notifyListeners(); 76 | } 77 | 78 | set initialized(bool value) { 79 | _initialized = value; 80 | notifyListeners(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/views/settings/server_functions_view.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:provider/provider.dart'; 6 | 7 | // Project imports: 8 | import 'package:msm/common_widgets.dart'; 9 | import 'package:msm/constants/colors.dart'; 10 | import 'package:msm/constants/constants.dart'; 11 | import 'package:msm/providers/app_provider.dart'; 12 | import 'package:msm/router/router_utils.dart'; 13 | import 'package:msm/ui_components/switch/switch.dart'; 14 | import 'package:msm/utils/server_functions.dart'; 15 | import 'package:msm/views/settings/settings_utils.dart'; 16 | 17 | class ServerFunctions extends StatefulWidget { 18 | const ServerFunctions({super.key}); 19 | 20 | @override 21 | State createState() => _ServerFunctionsState(); 22 | } 23 | 24 | class _ServerFunctionsState extends State { 25 | @override 26 | Widget build(BuildContext context) { 27 | AppService appService = Provider.of(context); 28 | return functions(context, appService); 29 | } 30 | } 31 | 32 | Widget functions(BuildContext context, AppService appService) { 33 | ServerFunctionsData serverFunctionsData = 34 | appService.storage.getServerFunctions; 35 | return Scaffold( 36 | key: ContextKeys.serverFunctionsPagekey, 37 | appBar: commonAppBar( 38 | backroute: Pages.settings.toPath, 39 | context: context, 40 | text: SettingsSubRoute.serverFunctions.toTitle), 41 | backgroundColor: CommonColors.commonWhiteColor, 42 | floatingActionButton: saveButton( 43 | onPressed: () => saveServerFunctions(serverFunctionsData, context)), 44 | floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, 45 | body: Column( 46 | children: [ 47 | CommonSwitch( 48 | text: 'Wake On Lan', 49 | value: serverFunctionsData.wakeOnLan, 50 | onChanged: (value) { 51 | serverFunctionsData.wakeOnLan = value; 52 | if (value) { 53 | showMessage( 54 | context: context, text: AppMessages.addMacAddress); 55 | } 56 | }), 57 | // CommonSwitch( 58 | // text: 'AutoUpdate Server', 59 | // value: serverFunctionsData.autoUpdate, 60 | // onChanged: (value) => serverFunctionsData.autoUpdate = value), 61 | serverFunctionsData.sendTokindle 62 | ? editSendToKindle(serverFunctionsData.sendTokindle) 63 | : CommonSwitch( 64 | text: 'Send To Kindle', 65 | value: serverFunctionsData.sendTokindle, 66 | onChanged: (value) => setKindleDetails(value)), 67 | ], 68 | )); 69 | } 70 | -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'dart:convert'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart' show rootBundle; 7 | 8 | // Package imports: 9 | import 'package:go_router/go_router.dart'; 10 | 11 | import 'package:msm/providers/file_listing_provider.dart'; 12 | import 'package:msm/providers/upload_provider.dart'; 13 | // Project imports: 14 | 15 | bool get appENV { 16 | bool isProd = const bool.fromEnvironment('dart.vm.product'); 17 | return isProd; 18 | } 19 | 20 | PopScope handleBackButton({ 21 | String? backRoute, 22 | required Widget child, 23 | required BuildContext context, 24 | UploadState? uploadState, 25 | FileListingState? fileListState, 26 | }) { 27 | // to handle the backroutes of the app 28 | return PopScope( 29 | canPop: false, 30 | onPopInvokedWithResult: (bool didPop, dynamic result) async { 31 | if (didPop && backRoute != null) { 32 | handleBack(context, uploadState, fileListState, backRoute); 33 | } 34 | }, 35 | child: child, 36 | ); 37 | } 38 | 39 | void handleBack(BuildContext context, UploadState? uploadState, 40 | FileListingState? fileListState, String backRoute) { 41 | if (uploadState != null) { 42 | uploadState.commonCalls; 43 | } 44 | if (fileListState != null) { 45 | fileListState.popPath; 46 | fileListState.clearSelection; 47 | fileListState.setNextPage = fileListState.lastPage; 48 | } 49 | if (backRoute.isNotEmpty) { 50 | GoRouter.of(context).go(backRoute); 51 | } 52 | } 53 | 54 | void hideKeyboard(BuildContext ctx) { 55 | try { 56 | FocusManager.instance.primaryFocus?.unfocus(); 57 | } catch (_) {} 58 | } 59 | 60 | String fileNameFromPath(String path) { 61 | return path.split('/').last.toString(); 62 | } 63 | 64 | /// Decodes the given [output] bytes to a UTF-8 string. 65 | /// 66 | /// Allows malformed UTF-8 sequences and replaces them with the Unicode 67 | /// replacement character (U+FFFD) to ensure a valid string is always returned. 68 | /// This prevents [FormatException] from being thrown for invalid byte sequences. 69 | String decodeOutput(Uint8List output) { 70 | return utf8.decode(output, allowMalformed: true); 71 | } 72 | 73 | /// Loads a shell script from the app's assets. 74 | /// 75 | /// [assetPath] The path to the script asset (e.g., 'assets/scripts/script.sh'). 76 | /// Returns the script content as a string. 77 | /// Throws [Exception] if the asset cannot be loaded or if the path is invalid. 78 | Future loadShellScript(String scriptPath) async { 79 | if (scriptPath.isEmpty) { 80 | throw ArgumentError('Asset path cannot be empty'); 81 | } 82 | try { 83 | return await rootBundle.loadString(scriptPath); 84 | } catch (e) { 85 | throw Exception('Failed to load script from $scriptPath: $e'); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/views/system_tools/system_tools.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | 8 | // Project imports: 9 | import 'package:msm/utils.dart'; 10 | import 'package:msm/common_widgets.dart'; 11 | import 'package:msm/constants/colors.dart'; 12 | import 'package:msm/router/router_utils.dart'; 13 | import 'package:msm/views/system_tools/system_tool_utils.dart'; 14 | 15 | class SystemTools extends StatelessWidget { 16 | const SystemTools({super.key}); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return handleBackButton(context: context, child: systemToolsMenu(context)); 21 | } 22 | } 23 | 24 | Widget systemToolsMenu(BuildContext context) { 25 | return Scaffold( 26 | appBar: commonAppBar( 27 | text: Pages.systemTools.toTitle, 28 | backroute: Pages.home.toPath, 29 | context: context), 30 | backgroundColor: CommonColors.commonWhiteColor, 31 | body: ListView( 32 | padding: commonListViewTopPadding, 33 | children: [ 34 | commonTile( 35 | icon: FontAwesomeIcons.terminal, 36 | title: 'Live Terminal', 37 | subtitle: 'To live interact with server terminal', 38 | onTap: () => 39 | context.goNamed(SystemToolsSubRoute.liveTerminal.toName)), 40 | commonTile( 41 | icon: FontAwesomeIcons.gears, 42 | title: 'Services', 43 | subtitle: 'Systemd services available in server', 44 | onTap: () => 45 | context.goNamed(SystemToolsSubRoute.services.toName)), 46 | // commonTile( 47 | // icon: FontAwesomeIcons.user, 48 | // title: 'User Management', 49 | // subtitle: 'Linux user management(Experimental)', 50 | // onTap: () { 51 | // print("pressed2"); 52 | // }), 53 | // commonTile( 54 | // icon: FontAwesomeIcons.download, 55 | // title: 'System Upgrade', 56 | // subtitle: 'Commands to update system OS', 57 | // onTap: () => systemUpdate(context)), 58 | commonTile( 59 | icon: FontAwesomeIcons.gaugeHigh, 60 | title: 'Speed Test', 61 | subtitle: 'Test your network speed', 62 | onTap: () => speedTestOutput(context)), 63 | // commonTile( 64 | // icon: FontAwesomeIcons.chartColumn, 65 | // title: 'Charts', 66 | // subtitle: 'See your system performance in graphs', 67 | // onTap: () { 68 | // print("pressed2"); 69 | // }) 70 | ], 71 | )); 72 | } 73 | -------------------------------------------------------------------------------- /lib/utils/ssh_keypair.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io' show File; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:basic_utils/basic_utils.dart'; 6 | import 'package:path_provider/path_provider.dart' 7 | show getExternalStorageDirectory; 8 | import 'package:pointycastle/export.dart'; 9 | 10 | class SSHKeyPair { 11 | final String privateKeyPem; 12 | final String publicKeyOpenSSH; 13 | SSHKeyPair(this.privateKeyPem, this.publicKeyOpenSSH); 14 | } 15 | 16 | String encodeRSAPublicKeyToOpenSSH(RSAPublicKey publicKey, 17 | {String comment = "flutter@msm"}) { 18 | final algorithm = utf8.encode("ssh-rsa"); 19 | final eBytes = _encodeBigInt(publicKey.exponent!); 20 | final nBytes = _encodeBigInt(publicKey.modulus!); 21 | 22 | final buffer = BytesBuilder(); 23 | buffer.add(_encodeLength(algorithm.length)); 24 | buffer.add(algorithm); 25 | 26 | buffer.add(_encodeLength(eBytes.length)); 27 | buffer.add(eBytes); 28 | 29 | buffer.add(_encodeLength(nBytes.length)); 30 | buffer.add(nBytes); 31 | 32 | final base64Key = base64.encode(buffer.toBytes()); 33 | return "ssh-rsa $base64Key $comment"; 34 | } 35 | 36 | /// Encode a BigInt to unsigned big-endian bytes 37 | Uint8List _encodeBigInt(BigInt number) { 38 | final hex = number.toRadixString(16); 39 | final evenHex = hex.length % 2 == 0 ? hex : "0$hex"; 40 | final bytes = Uint8List.fromList(List.generate(evenHex.length ~/ 2, 41 | (i) => int.parse(evenHex.substring(i * 2, i * 2 + 2), radix: 16))); 42 | if (bytes.isNotEmpty && bytes[0] & 0x80 != 0) { 43 | // add leading 0x00 for sign bit 44 | return Uint8List.fromList([0, ...bytes]); 45 | } 46 | return bytes; 47 | } 48 | 49 | /// Encode length as 4-byte big-endian 50 | Uint8List _encodeLength(int length) { 51 | final b = ByteData(4)..setUint32(0, length); 52 | return b.buffer.asUint8List(); 53 | } 54 | 55 | SSHKeyPair generateRSAKeyPair({int bitLength = 2048}) { 56 | final keyGen = RSAKeyGenerator() 57 | ..init(ParametersWithRandom( 58 | RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64), 59 | SecureRandom('Fortuna') 60 | ..seed(KeyParameter(Uint8List.fromList( 61 | List.generate(32, (_) => DateTime.now().millisecond)))), 62 | )); 63 | 64 | final pair = keyGen.generateKeyPair(); 65 | final private = pair.privateKey as RSAPrivateKey; 66 | final public = pair.publicKey as RSAPublicKey; 67 | final privatePem = CryptoUtils.encodeRSAPrivateKeyToPemPkcs1(private); 68 | final publicOpenSSH = 69 | encodeRSAPublicKeyToOpenSSH(public, comment: 'com.prinzpiuz.msm'); 70 | 71 | return SSHKeyPair(privatePem, publicOpenSSH); 72 | } 73 | 74 | Future savePrivateKeyLocally(String privateKeyPem) async { 75 | final dir = await getExternalStorageDirectory(); 76 | final file = File('${dir?.path}/id_rsa.key'); 77 | await file.writeAsString(privateKeyPem, flush: true); 78 | return file.path; 79 | } 80 | -------------------------------------------------------------------------------- /lib/utils/storage.dart: -------------------------------------------------------------------------------- 1 | // Dart imports: 2 | import 'dart:convert'; 3 | 4 | // Package imports: 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | // Project imports: 8 | import 'package:msm/utils/folder_configuration.dart'; 9 | import 'package:msm/utils/send_to_kindle.dart'; 10 | import 'package:msm/utils/server_details.dart'; 11 | import 'package:msm/utils/server_functions.dart'; 12 | 13 | enum StorageKeys { 14 | firstTime, 15 | serverData, 16 | serverFunctions, 17 | folderConfigurations, 18 | kindleData, 19 | serverOS 20 | } 21 | 22 | extension StorageKeysExtension on StorageKeys { 23 | String get key { 24 | switch (this) { 25 | case StorageKeys.firstTime: 26 | return "firsttime"; 27 | case StorageKeys.serverData: 28 | return "serverData"; 29 | case StorageKeys.serverFunctions: 30 | return "serverFunctions"; 31 | case StorageKeys.folderConfigurations: 32 | return "folderConfigurations"; 33 | case StorageKeys.kindleData: 34 | return "kindleData"; 35 | case StorageKeys.serverOS: 36 | return "serverOS"; 37 | } 38 | } 39 | } 40 | 41 | class Storage { 42 | SharedPreferences prefs; 43 | 44 | Storage({required this.prefs}); 45 | 46 | void clearAll() => prefs.clear(); 47 | 48 | Map? _getJson(String key) { 49 | final String? jsonString = prefs.getString(key); 50 | if (jsonString != null) { 51 | Map stringToJson = jsonDecode(jsonString); 52 | return stringToJson; 53 | } 54 | return null; 55 | } 56 | 57 | void saveObject(String key, dynamic object) async { 58 | String objectToString = jsonEncode(object); 59 | await prefs.setString(key, objectToString); 60 | } 61 | 62 | ServerData get getServerData { 63 | Map? data = _getJson(StorageKeys.serverData.key); 64 | if (data != null) { 65 | return ServerData.fromJson(data); 66 | } 67 | return ServerData(); 68 | } 69 | 70 | ServerFunctionsData get getServerFunctions { 71 | Map? data = _getJson(StorageKeys.serverFunctions.key); 72 | if (data != null) { 73 | return ServerFunctionsData.fromJson(data); 74 | } 75 | return ServerFunctionsData(); 76 | } 77 | 78 | FolderConfiguration get getFolderConfigurations { 79 | Map? data = _getJson(StorageKeys.folderConfigurations.key); 80 | if (data != null) { 81 | return FolderConfiguration.fromJson(data); 82 | } 83 | return FolderConfiguration(); 84 | } 85 | 86 | KindleData get getKindleData { 87 | Map? data = _getJson(StorageKeys.kindleData.key); 88 | if (data != null) { 89 | return KindleData.fromJson(data); 90 | } 91 | return KindleData(); 92 | } 93 | 94 | Future getFirstTimeInstall() async => 95 | prefs.getBool(StorageKeys.firstTime.key) ?? true; 96 | void setFirstTimeInstall(bool value) async => 97 | prefs.setBool(StorageKeys.firstTime.key, value); 98 | } 99 | -------------------------------------------------------------------------------- /lib/ui_components/text/textstyles.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart' show TextStyle, FontWeight, Color; 3 | 4 | // Package imports: 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | import 'package:google_fonts/google_fonts.dart'; 7 | 8 | // Project imports: 9 | import 'package:msm/constants/colors.dart'; 10 | 11 | class AppTextStyles { 12 | AppTextStyles._(); 13 | 14 | static TextStyle inputTextStyle() { 15 | return medium( 16 | TextFormColors.inputTextColor, 17 | 14, 18 | ); 19 | } 20 | 21 | static TextStyle inputHintTextStyle() { 22 | return medium( 23 | TextFormColors.inputHintTextColor, 24 | 14, 25 | ); 26 | } 27 | 28 | static TextStyle inputHelperTextStyle() { 29 | return medium( 30 | TextFormColors.inputHelperTextColor, 31 | 11, 32 | ); 33 | } 34 | 35 | static TextStyle inputErrorTextStyle() { 36 | return medium(TextFormColors.inputErrorTextColor, 11, height: 2); 37 | } 38 | 39 | static TextStyle extraBold(Color textColor, double fontSize, 40 | {double? letterSpacing, double? height}) { 41 | return GoogleFonts.openSans( 42 | textStyle: TextStyle( 43 | color: textColor, 44 | fontSize: fontSize.sp, 45 | letterSpacing: letterSpacing, 46 | height: height?.sp, 47 | fontWeight: FontWeight.w800), 48 | ); 49 | } 50 | 51 | static TextStyle bold(Color textColor, double fontSize, 52 | {double? letterSpacing, double? height}) { 53 | return GoogleFonts.openSans( 54 | textStyle: TextStyle( 55 | color: textColor, 56 | fontSize: fontSize.sp, 57 | letterSpacing: letterSpacing, 58 | height: height?.sp, 59 | fontWeight: FontWeight.w700), 60 | ); 61 | } 62 | 63 | static TextStyle medium(Color textColor, double fontSize, 64 | {double? letterSpacing, double? height}) { 65 | return GoogleFonts.openSans( 66 | textStyle: TextStyle( 67 | color: textColor, 68 | fontSize: fontSize.sp, 69 | letterSpacing: letterSpacing, 70 | height: height?.sp, 71 | fontWeight: FontWeight.w500), 72 | ); 73 | } 74 | 75 | static TextStyle regular(Color textColor, double fontSize, 76 | {double? letterSpacing, double? height}) { 77 | return GoogleFonts.openSans( 78 | textStyle: TextStyle( 79 | color: textColor, 80 | fontSize: fontSize.sp, 81 | letterSpacing: letterSpacing, 82 | height: height?.sp, 83 | fontWeight: FontWeight.w400), 84 | ); 85 | } 86 | 87 | static TextStyle light(Color textColor, double fontSize, 88 | {double? letterSpacing, double? height}) { 89 | return GoogleFonts.openSans( 90 | textStyle: TextStyle( 91 | color: textColor, 92 | fontSize: fontSize.sp, 93 | letterSpacing: letterSpacing, 94 | height: height?.sp, 95 | fontWeight: FontWeight.w300), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /.github/workflows/build_and_release.yml: -------------------------------------------------------------------------------- 1 | name: Build And Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "release" 7 | 8 | env: 9 | FLUTTER_VERSION: "3.38.3" 10 | 11 | jobs: 12 | tag: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: "0" 18 | - name: Bump version and push tag 19 | id: autoversion 20 | uses: ietf-tools/semver-action@v1 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | branch: main 24 | skipInvalidTags: true 25 | outputs: 26 | new_tag: ${{ steps.autoversion.outputs.nextStrict }} 27 | build_and_release: 28 | needs: tag 29 | runs-on: ubuntu-latest 30 | name: Build and Release Apps 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 0 36 | - name: Retrieve base64 keystore and decode it to a file 37 | env: 38 | KEYSTORE_BASE64: ${{ secrets.KEYSTORE_FILE_BASE64 }} 39 | run: echo "$KEYSTORE_BASE64" | base64 --decode > "${{ github.workspace }}/android-keystore.jks" 40 | - name: Create keystore.properties file 41 | env: 42 | KEYSTORE_PROPERTIES_PATH: ${{ github.workspace }}/android/keystore.properties 43 | run: | 44 | echo 'storeFile=${{ github.workspace }}/android-keystore.jks' > $KEYSTORE_PROPERTIES_PATH 45 | echo 'keyAlias=${{ secrets.KEYSTORE_KEY_ALIAS }}' >> $KEYSTORE_PROPERTIES_PATH 46 | echo 'storePassword=${{ secrets.KEYSTORE_PASSWORD }}' >> $KEYSTORE_PROPERTIES_PATH 47 | echo 'keyPassword=${{ secrets.KEYSTORE_KEY_PASSWORD }}' >> $KEYSTORE_PROPERTIES_PATH 48 | - name: Build 49 | uses: subosito/flutter-action@v2 50 | with: 51 | flutter-version: ${{ env.FLUTTER_VERSION }} 52 | channel: "stable" 53 | - run: flutter --version 54 | - run: flutter pub get 55 | - run: flutter build apk --split-per-abi --no-tree-shake-icons --build-name=${{ needs.tag.outputs.new_tag }} --release 56 | - run: flutter pub get 57 | - run: flutter build appbundle 58 | - name: Release 59 | uses: softprops/action-gh-release@v1 60 | with: 61 | body_path: CHANGELOG.md 62 | tag_name: ${{ needs.tag.outputs.new_tag }} 63 | prerelease: false 64 | name: MSM-${{ needs.tag.outputs.new_tag }} 65 | files: build/app/outputs/flutter-apk/*.apk, build/app/outputs/bundle/release/app-release.aab 66 | token: ${{ secrets.GITHUB_TOKEN }} 67 | - name: Notify Telegram 68 | uses: appleboy/telegram-action@master 69 | with: 70 | to: ${{ secrets.TELEGRAM_TO }} 71 | token: ${{ secrets.TELEGRAM_TOKEN }} 72 | message: | 73 | Alert For New Release! 74 | MSM-${{ needs.tag.outputs.new_tag }} 75 | See changes: https://github.com/prinzpiuz/MSM/blob/master/CHANGELOG.md 76 | See release: https://github.com/prinzpiuz/MSM/releases/tag/${{ needs.tag.outputs.new_tag }} 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

MSM

3 |

All in one manager for your media server

4 | 5 |

6 | 7 | 8 | RB Status 9 |
10 | 11 | Get it on F-Droid 12 | 13 | 14 | Get it on F-Droid 15 | 16 |

17 |
18 |

ScreenshotsDescriptionFeaturesContributionReleases

19 | 20 |
21 | 22 | ## Description 23 | 24 | Managing your media server just got a lot easier! MSM is a handy Android app that acts as a wrapper around your favorite media servers like Emby, Jellyfin, Kodi, or Plex. 25 | 26 | **What It Does** 27 | With MSM, you can easily manage your media files directly from your phone. Whether you need to add, remove, or edit files, MSM helps you perform these tasks without ever having to log into your server. It also allows you to manage server services, giving you control from the palm of your hand. 28 | 29 | **How to Get Started** 30 | 31 | All you need is an Android mobile phone and a media server—as long as they're on the same network, you're good to go! 📱💻 32 | 33 | **Note**: This is app intended to use in local network only, Tested mostly with my home lab setup 34 | 35 | ## Screenshots 36 | 37 | - [latest](fastlane/metadata/android/en-US/images/phoneScreenshots) 38 | 39 | ### Features 40 | 41 | - Works on top of SSH connection to server 42 | - Password less authentication 43 | - Create and update SSH keys 44 | - CRUD options on files 45 | - Send To Kindle for e-books 46 | - Uploads/downloads run as background tasks 47 | - Manage systemd services 48 | - Wake on LAN support 49 | - Notifications for downloads/uploads 50 | - Live Terminal 51 | - Real Time server details on home screen 52 | 53 | ## Contribution 54 | 55 | - Whether you have ideas, design changes, code cleaning, or real heavy code changes, Help is always welcome. 56 | - Please join this [Telegram](https://t.me/joinchat/FDVzK06Rt7vsNQLBLi2icw) group for further discussion 57 | - The contributing guide is available [here](https://github.com/prinzpiuz/MSM_mobile/blob/main/CONTRIBUTING.md) 58 | -------------------------------------------------------------------------------- /lib/views/settings/server_details/ssh_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 4 | 5 | import 'package:msm/common_widgets.dart'; 6 | import 'package:msm/constants/colors.dart'; 7 | import 'package:msm/constants/constants.dart' show AppMessages; 8 | import 'package:msm/ui_components/textfield/textfield.dart'; 9 | import 'package:msm/ui_components/textfield/validators.dart'; 10 | import 'package:msm/utils/server.dart' show uploadPublicKey; 11 | import 'package:msm/utils/server_details.dart'; 12 | import 'package:msm/utils/ssh_keypair.dart'; 13 | 14 | Widget sshKeyField( 15 | BuildContext context, 16 | ServerData serverData, 17 | void Function(ServerData, {String? fileSelected}) onSSHFileSelected, 18 | TextEditingController sshKeyController) { 19 | return AppTextField.commonTextField( 20 | controller: sshKeyController, 21 | readOnly: true, 22 | suffixIcon: IconButton( 23 | icon: const Icon( 24 | Icons.folder_open, 25 | color: CommonColors.commonBlackColor, 26 | ), 27 | tooltip: "Browse", 28 | onPressed: () => onSSHFileSelected(serverData), 29 | ), 30 | validator: valueNeeded, 31 | keyboardType: TextInputType.none, 32 | labelText: 'SSH Private Key', 33 | hintText: 'Select your private key file', 34 | ); 35 | } 36 | 37 | Future generateAndSaveSSHKey(BuildContext context, ServerData serverData, 38 | void Function(ServerData, {String? fileSelected}) onSSHFileSelected) async { 39 | if (!serverData.detailsForKeyUploadAvailable) { 40 | showMessage(context: context, text: AppMessages.fillDetails); 41 | return; 42 | } 43 | final password = await askPassword(context: context); 44 | if (password == null || password.isEmpty) return; 45 | 46 | final keyPair = generateRSAKeyPair(); 47 | 48 | try { 49 | await uploadPublicKey( 50 | host: serverData.serverHost, 51 | port: int.parse(serverData.portNumber), 52 | username: serverData.username, 53 | password: password, 54 | publicKey: keyPair.publicKeyOpenSSH, 55 | ); 56 | if (context.mounted) { 57 | showMessage(context: context, text: AppMessages.sshKeyUploaded); 58 | } 59 | final localKeyPath = await savePrivateKeyLocally(keyPair.privateKeyPem); 60 | onSSHFileSelected(serverData, fileSelected: localKeyPath); 61 | } catch (e) { 62 | if (context.mounted) { 63 | showMessage(context: context, text: AppMessages.sshKeyNotUploaded); 64 | } 65 | return; 66 | } 67 | } 68 | 69 | Widget generateKeyPairButton( 70 | BuildContext context, 71 | ServerData serverData, 72 | void Function(ServerData, {String? fileSelected}) onSSHFileSelected, 73 | ) { 74 | return Padding( 75 | padding: EdgeInsets.only(left: 20.w, right: 20.w, top: 10.h), 76 | child: outlinedTextButton( 77 | text: "Generate SSH Key Pair", 78 | onPressed: () => generateAndSaveSSHKey( 79 | context, 80 | serverData, 81 | onSSHFileSelected, 82 | ), 83 | icon: Icon( 84 | Icons.vpn_key, 85 | color: CommonColors.commonBlackColor, 86 | )), 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ configuration file 2 | # https://git-cliff.org/docs/configuration 3 | 4 | # [remote.github] 5 | # owner = "orhun" 6 | # repo = "git-cliff" 7 | # token = "" 8 | 9 | [changelog] 10 | # A Tera template to be rendered for each release in the changelog. 11 | # See https://keats.github.io/tera/docs/#introduction 12 | body = """ 13 | ## What's Changed 14 | 15 | {%- if version %} in {{ version }}{%- endif -%} 16 | {% for commit in commits %} 17 | {% if commit.remote.pr_title -%} 18 | {%- set commit_message = commit.remote.pr_title -%} 19 | {%- else -%} 20 | {%- set commit_message = commit.message -%} 21 | {%- endif -%} 22 | * {{ commit_message | split(pat="\n") | first | trim }}\ 23 | {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%} 24 | {% if commit.remote.pr_number %} in \ 25 | [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \ 26 | {%- endif %} 27 | {%- endfor -%} 28 | 29 | {%- if github -%} 30 | {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} 31 | {% raw %}\n{% endraw -%} 32 | ## New Contributors 33 | {%- endif %}\ 34 | {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} 35 | * @{{ contributor.username }} made their first contribution 36 | {%- if contributor.pr_number %} in \ 37 | [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ 38 | {%- endif %} 39 | {%- endfor -%} 40 | {%- endif -%} 41 | 42 | {% if version %} 43 | {% if previous.version %} 44 | **Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }} 45 | {% endif %} 46 | {% else -%} 47 | {% raw %}\n{% endraw %} 48 | {% endif %} 49 | 50 | {%- macro remote_url() -%} 51 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} 52 | {%- endmacro -%} 53 | """ 54 | # Remove leading and trailing whitespaces from the changelog's body. 55 | trim = true 56 | # A Tera template to be rendered as the changelog's footer. 57 | # See https://keats.github.io/tera/docs/#introduction 58 | footer = """ 59 | 60 | """ 61 | # An array of regex based postprocessors to modify the changelog. 62 | # Replace the placeholder `` with a URL. 63 | postprocessors = [] 64 | 65 | [git] 66 | # Parse commits according to the conventional commits specification. 67 | # See https://www.conventionalcommits.org 68 | conventional_commits = false 69 | # Exclude commits that do not match the conventional commits specification. 70 | filter_unconventional = true 71 | # Split commits on newlines, treating each line as an individual commit. 72 | split_commits = false 73 | # An array of regex based parsers to modify commit messages prior to further processing. 74 | commit_preprocessors = [{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }] 75 | # Exclude commits that are not matched by any commit parser. 76 | filter_commits = false 77 | # Order releases topologically instead of chronologically. 78 | topo_order = false 79 | # Order of commits in each group/release within the changelog. 80 | # Allowed values: newest, oldest 81 | sort_commits = "newest" 82 | -------------------------------------------------------------------------------- /lib/providers/upload_provider.dart: -------------------------------------------------------------------------------- 1 | // Dart imports: 2 | import 'dart:io'; 3 | 4 | // Flutter imports: 5 | import 'package:flutter/material.dart'; 6 | 7 | // Project imports: 8 | import 'package:msm/utils/file_manager.dart'; 9 | import 'package:msm/utils/file_upload.dart'; 10 | import 'package:msm/views/upload_pages/upload_page_utils.dart'; 11 | 12 | class UploadState with ChangeNotifier { 13 | UploadCatogories _category = UploadCatogories.movies; 14 | List _categoryExtensions = FileManager.allowedMovieExtensions; 15 | String _currentListing = UploadCatogories.movies.getTitle; 16 | bool _recursive = false; 17 | String newFolderName = ""; 18 | bool empty = false; 19 | bool toCustomFolder = false; 20 | String customPath = ""; 21 | final List _nextFilesDirectory = []; 22 | final List _directories = FileManager.defaultDirectories; 23 | final FileUploadData fileUploadData = FileUploadData(); 24 | final List _trackRemoteDirectory = []; 25 | final List _newFolders = []; 26 | 27 | UploadState(); 28 | 29 | UploadCatogories get getCategory => _category; 30 | List get getCategoryExtensions => _categoryExtensions; 31 | List get getCategoryDirectories => _directories; 32 | String get getCurrentListing => _currentListing; 33 | bool get getRecursive => _recursive; 34 | List get getNextFilesDirectory => _nextFilesDirectory; 35 | List get traversedDirectories => _trackRemoteDirectory; 36 | List get newFoldersToCreate => _newFolders; 37 | 38 | set setCategory(UploadCatogories currentCategory) { 39 | _category = currentCategory; 40 | notifyListeners(); 41 | } 42 | 43 | set setCategoryExtensions(List currentCategoryExtensions) { 44 | _categoryExtensions = currentCategoryExtensions; 45 | notifyListeners(); 46 | } 47 | 48 | set setCurrentListing(String currentCategory) { 49 | _currentListing = currentCategory; 50 | } 51 | 52 | set setRecursive(bool recursive) { 53 | _recursive = recursive; 54 | } 55 | 56 | set setNextFilesDirectory(Directory nextDirectory) { 57 | _nextFilesDirectory.add(nextDirectory); 58 | } 59 | 60 | void addRemoteDirectoru(String directory) { 61 | if (directory.isNotEmpty) { 62 | _trackRemoteDirectory.add(directory); 63 | } 64 | } 65 | 66 | void addNewFolderName(String name) { 67 | if (name.isNotEmpty) { 68 | _newFolders.add(name); 69 | } 70 | } 71 | 72 | void get clearNewFolder => _newFolders.clear(); 73 | 74 | void get clearPaths => _trackRemoteDirectory.clear(); 75 | 76 | void popLastDirectory() { 77 | if (_nextFilesDirectory.isNotEmpty) { 78 | _nextFilesDirectory.removeLast(); 79 | } else { 80 | _recursive = false; 81 | } 82 | notifyListeners(); 83 | } 84 | 85 | void get fileAddOrRemove { 86 | notifyListeners(); 87 | } 88 | 89 | void get commonClear { 90 | clearPaths; 91 | empty = false; 92 | clearNewFolder; 93 | } 94 | 95 | void get commonCalls { 96 | commonClear; 97 | fileUploadData.clear; 98 | popLastDirectory(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | def keystoreProperties = new Properties() 7 | def keystorePropertiesFile = rootProject.file('keystore.properties') 8 | if (keystorePropertiesFile.exists()) { 9 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 10 | } 11 | 12 | def localProperties = new Properties() 13 | def localPropertiesFile = rootProject.file('local.properties') 14 | if (localPropertiesFile.exists()) { 15 | localPropertiesFile.withReader('UTF-8') { reader -> 16 | localProperties.load(reader) 17 | } 18 | } 19 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 20 | if (flutterVersionCode == null) { 21 | flutterVersionCode = '1' 22 | } 23 | 24 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 25 | if (flutterVersionName == null) { 26 | flutterVersionName = '1.0' 27 | } 28 | 29 | 30 | android { 31 | namespace "com.prinzpiuz.msm" 32 | compileSdkVersion 36 33 | ndkVersion flutter.ndkVersion 34 | 35 | compileOptions { 36 | //added for the setup of local notification 37 | coreLibraryDesugaringEnabled true 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.prinzpiuz.msm" 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-build-configuration. 54 | minSdkVersion flutter.minSdkVersion 55 | targetSdkVersion flutter.targetSdkVersion 56 | versionCode flutterVersionCode.toInteger() 57 | versionName flutterVersionName 58 | multiDexEnabled true 59 | } 60 | signingConfigs { 61 | release { 62 | keyAlias keystoreProperties['keyAlias'] 63 | keyPassword keystoreProperties['keyPassword'] 64 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 65 | storePassword keystoreProperties['storePassword'] 66 | } 67 | } 68 | 69 | buildTypes { 70 | debug { 71 | applicationIdSuffix ".debug" 72 | versionNameSuffix "-debug" 73 | } 74 | release { 75 | minifyEnabled false 76 | shrinkResources false 77 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 78 | signingConfig signingConfigs.release 79 | } 80 | } 81 | 82 | dependenciesInfo { 83 | // Disables dependency metadata when building APKs. 84 | includeInApk = false 85 | // Disables dependency metadata when building Android App Bundles. 86 | includeInBundle = false 87 | } 88 | } 89 | 90 | flutter { 91 | source '../..' 92 | } 93 | 94 | dependencies { 95 | implementation 'androidx.window:window:1.0.0' 96 | implementation 'androidx.window:window-java:1.0.0' 97 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' 98 | } 99 | -------------------------------------------------------------------------------- /lib/views/system_tools/live_terminal.dart: -------------------------------------------------------------------------------- 1 | // Dart imports: 2 | import 'dart:convert'; 3 | 4 | // Flutter imports: 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | 8 | // Package imports: 9 | import 'package:dartssh2/dartssh2.dart'; 10 | import 'package:xterm/xterm.dart'; 11 | 12 | // Project imports: 13 | import 'package:msm/providers/app_provider.dart'; 14 | 15 | class LiveTerminalPage extends StatelessWidget { 16 | final AppService appService; 17 | const LiveTerminalPage({super.key, required this.appService}); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return LiveTerminal(appService: appService); 22 | } 23 | } 24 | 25 | class LiveTerminal extends StatefulWidget { 26 | final AppService appService; 27 | 28 | const LiveTerminal({super.key, required this.appService}); 29 | @override 30 | LiveTerminalState createState() => LiveTerminalState(); 31 | } 32 | 33 | class LiveTerminalState extends State { 34 | final terminal = Terminal( 35 | platform: TerminalTargetPlatform.linux, 36 | maxLines: 10000, 37 | ); 38 | 39 | Future initTerminal() async { 40 | terminal.write('Connecting...\r\n'); 41 | SSHClient? client = await widget.appService.server.connect(); 42 | if (client != null) { 43 | terminal.write('Connected\r\n'); 44 | final session = await client.shell( 45 | pty: SSHPtyConfig( 46 | width: terminal.viewWidth, 47 | height: terminal.viewHeight, 48 | ), 49 | ); 50 | 51 | terminal.buffer.clear(); 52 | terminal.buffer.setCursor(0, 0); 53 | 54 | terminal.onOutput = (data) { 55 | session.write(utf8.encode(data)); 56 | }; 57 | 58 | _listen(session.stdout); 59 | _listen(session.stderr); 60 | 61 | await session.done; 62 | if (mounted) { 63 | Navigator.of(context).pop(); 64 | } 65 | } else { 66 | terminal.write('Connection Failed\r\n'); 67 | } 68 | } 69 | 70 | void _listen(Stream stream) { 71 | stream 72 | .cast>() 73 | .transform(const Utf8Decoder()) 74 | .listen(terminal.write); 75 | } 76 | 77 | final terminalController = TerminalController(); 78 | 79 | @override 80 | void initState() { 81 | super.initState(); 82 | initTerminal(); 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | return Scaffold( 88 | backgroundColor: Colors.transparent, 89 | body: SafeArea( 90 | child: TerminalView( 91 | terminal, 92 | controller: terminalController, 93 | autofocus: true, 94 | onSecondaryTapDown: (details, offset) async { 95 | final selection = terminalController.selection; 96 | if (selection != null) { 97 | final text = terminal.buffer.getText(selection); 98 | terminalController.clearSelection(); 99 | await Clipboard.setData(ClipboardData(text: text)); 100 | } else { 101 | final data = await Clipboard.getData('text/plain'); 102 | final text = data?.text; 103 | if (text != null) { 104 | terminal.paste(text); 105 | } 106 | } 107 | }, 108 | ), 109 | ), 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /assets/svgs/msm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/ui_components/textfield/input_formatters.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/services.dart'; 3 | 4 | class IpAddressInputFormatter extends TextInputFormatter { 5 | //for reference look here 6 | //https://stackoverflow.com/questions/69230821/how-to-make-a-textinputformatter-mask-for-ipaddress-in-flutter 7 | 8 | @override 9 | TextEditingValue formatEditUpdate( 10 | TextEditingValue oldValue, TextEditingValue newValue) { 11 | var text = newValue.text; 12 | 13 | if (newValue.selection.baseOffset == 0) { 14 | return newValue; 15 | } 16 | 17 | int dotCounter = 0; 18 | var buffer = StringBuffer(); 19 | String ipField = ""; 20 | 21 | for (int i = 0; i < text.length; i++) { 22 | if (dotCounter < 4) { 23 | if (text[i] != ".") { 24 | ipField += text[i]; 25 | if (ipField.length < 3) { 26 | buffer.write(text[i]); 27 | } else if (ipField.length == 3) { 28 | if (int.parse(ipField) <= 255) { 29 | buffer.write(text[i]); 30 | } else { 31 | if (dotCounter < 3) { 32 | buffer.write("."); 33 | dotCounter++; 34 | buffer.write(text[i]); 35 | ipField = text[i]; 36 | } 37 | } 38 | } else if (ipField.length == 4) { 39 | if (dotCounter < 3) { 40 | buffer.write("."); 41 | dotCounter++; 42 | buffer.write(text[i]); 43 | ipField = text[i]; 44 | } 45 | } 46 | } else { 47 | if (dotCounter < 3) { 48 | buffer.write("."); 49 | dotCounter++; 50 | ipField = ""; 51 | } 52 | } 53 | } 54 | } 55 | 56 | var string = buffer.toString(); 57 | return newValue.copyWith( 58 | text: string, 59 | selection: TextSelection.collapsed(offset: string.length)); 60 | } 61 | } 62 | 63 | class UpperCaseTextFormatter extends TextInputFormatter { 64 | //for reference look here 65 | //https://stackoverflow.com/a/49239762/8368092 66 | @override 67 | TextEditingValue formatEditUpdate( 68 | TextEditingValue oldValue, TextEditingValue newValue) { 69 | return TextEditingValue( 70 | text: newValue.text.toUpperCase(), 71 | selection: newValue.selection, 72 | ); 73 | } 74 | } 75 | 76 | class MACAddressInputFormatter extends TextInputFormatter { 77 | @override 78 | TextEditingValue formatEditUpdate( 79 | TextEditingValue oldValue, TextEditingValue newValue) { 80 | var text = newValue.text; 81 | 82 | if (newValue.selection.baseOffset == 0) { 83 | return newValue; 84 | } 85 | 86 | int colonCounter = 0; 87 | var buffer = StringBuffer(); 88 | String macField = ""; 89 | 90 | for (int i = 0; i < text.length; i++) { 91 | if (colonCounter < 6 && buffer.toString().length < 17) { 92 | if (text[i] != ":") { 93 | macField += text[i]; 94 | if (macField.length < 3) { 95 | buffer.write(text[i]); 96 | } else if (macField.length == 3) { 97 | buffer.write(":"); 98 | colonCounter++; 99 | buffer.write(text[i]); 100 | macField = ""; 101 | } 102 | } else { 103 | if (colonCounter < 6) { 104 | buffer.write(":"); 105 | colonCounter++; 106 | macField = ""; 107 | } 108 | } 109 | } 110 | } 111 | 112 | var string = buffer.toString(); 113 | return newValue.copyWith( 114 | text: string, 115 | selection: TextSelection.collapsed(offset: string.length)); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/utils/server.dart: -------------------------------------------------------------------------------- 1 | // Package imports: 2 | import 'dart:io' show File; 3 | 4 | import 'package:dartssh2/dartssh2.dart'; 5 | 6 | // Project imports: 7 | import 'package:msm/constants/constants.dart'; 8 | import 'package:msm/utils/commands/commands.dart' show Commands; 9 | import 'package:msm/utils/folder_configuration.dart'; 10 | import 'package:msm/utils/server_details.dart'; 11 | import 'package:msm/utils/server_functions.dart'; 12 | 13 | enum ServerState { 14 | disconnected, 15 | connecting, 16 | connected, 17 | failed; 18 | 19 | bool get shouldConnect => 20 | this == ServerState.disconnected || this == ServerState.failed; 21 | 22 | String get message { 23 | switch (this) { 24 | case ServerState.connected: 25 | return AppConstants.connected; 26 | case ServerState.disconnected: 27 | return AppConstants.disconnected; 28 | case ServerState.connecting: 29 | return AppConstants.connecting; 30 | case ServerState.failed: 31 | return AppConstants.notAvailable; 32 | } 33 | } 34 | } 35 | 36 | class Server { 37 | ServerData serverData; 38 | FolderConfiguration folderConfiguration; 39 | ServerFunctionsData serverFunctionsData; 40 | SSHClient? _client; 41 | ServerState state = ServerState.disconnected; 42 | 43 | Server( 44 | {required this.serverData, 45 | required this.folderConfiguration, 46 | required this.serverFunctionsData}); 47 | 48 | Future connect() async { 49 | try { 50 | final username = serverData.username.trim(); 51 | final host = serverData.serverHost.trim(); 52 | final socket = await SSHSocket.connect( 53 | host, 54 | int.tryParse(serverData.portNumber) ?? 22, 55 | timeout: const Duration(seconds: 5), 56 | ); 57 | 58 | SSHClient client; 59 | if (serverData.cachedPrivateKey != null) { 60 | client = SSHClient( 61 | socket, 62 | username: username, 63 | identities: serverData.cachedPrivateKey, 64 | ); 65 | } else { 66 | if (serverData.privateKeyPath.isNotEmpty) { 67 | final privateKeyFile = File(serverData.privateKeyPath); 68 | final privateKeyContent = await privateKeyFile.readAsString(); 69 | serverData.cachedPrivateKey = SSHKeyPair.fromPem(privateKeyContent); 70 | final identities = SSHKeyPair.fromPem(privateKeyContent); 71 | 72 | client = SSHClient( 73 | socket, 74 | username: username, 75 | identities: identities, 76 | ); 77 | } else { 78 | client = SSHClient( 79 | socket, 80 | username: username, 81 | onPasswordRequest: () => serverData.rootPassword, 82 | ); 83 | } 84 | } 85 | 86 | _client = client; 87 | state = ServerState.connected; 88 | return _client; 89 | } catch (_) { 90 | state = ServerState.failed; 91 | return null; 92 | } 93 | } 94 | 95 | void close() async { 96 | if (_client != null) { 97 | _client?.close(); 98 | await _client?.done; 99 | _client = null; 100 | state = ServerState.disconnected; 101 | } 102 | } 103 | } 104 | 105 | Future uploadPublicKey({ 106 | required String host, 107 | required int port, 108 | required String username, 109 | required String password, 110 | required String publicKey, 111 | }) async { 112 | final socket = await SSHSocket.connect(host, port); 113 | final client = SSHClient( 114 | socket, 115 | username: username, 116 | onPasswordRequest: () => password, 117 | ); 118 | 119 | final session = await client.execute(Commands.copySSHKey(publicKey)); 120 | 121 | await session.done; 122 | client.close(); 123 | } 124 | -------------------------------------------------------------------------------- /lib/utils/commands/commands.dart: -------------------------------------------------------------------------------- 1 | /// A collection of predefined shell commands for system management operations. 2 | /// All commands are designed to be executed on Linux systems. 3 | class Commands { 4 | // File and folder operations 5 | /// Deletes files or folders recursively and forcefully. 6 | static const deleteFileOrFolders = "rm -rf"; 7 | 8 | /// Renames or moves files/folders. 9 | static const rename = "mv"; 10 | 11 | /// Encodes data to base64 without line wrapping. 12 | static const base64 = "base64 --wrap=0"; 13 | 14 | // Service management commands 15 | /// Lists all services with status information, formatted with commas and 'end' marker. 16 | static const String getServices = 17 | "systemctl --type=service -a --plain | awk '{ print \$1, \",\" , \$3, \",\" , \$4, \",\" , \$5\$6\$7\$8\$9,\"end\"}'"; 18 | 19 | /// Starts a systemd service. 20 | static const serviceStart = "systemctl start"; 21 | 22 | /// Gets the status of a systemd service. 23 | static const serviceStatus = "systemctl status"; 24 | 25 | /// Stops a systemd service. 26 | static const serviceStop = "systemctl stop"; 27 | 28 | /// Restarts a systemd service. 29 | static const serviceRestart = "systemctl restart"; 30 | 31 | // Network commands 32 | /// Placeholder for ping command (currently empty - set as needed). 33 | static const ping = ""; 34 | 35 | /// Runs speed test and outputs JSON results. 36 | static const speedTest = "speedtest-cli --secure --bytes --json"; 37 | 38 | /// Checks if a folder exists on the filesystem. 39 | /// Properly escapes the folder path to handle special characters. 40 | static String folderExist(String folder) { 41 | final escapedFolder = folder.replaceAll("'", "\\'"); 42 | return "if [ -d '$escapedFolder' ]; then echo 'Exists'; else echo 'Not found'; fi"; 43 | } 44 | 45 | /// Prepends an identifier to a command output for easy parsing. 46 | static String addIdentifier(String command, String identifier) { 47 | return "echo -n '$identifier:';$command"; 48 | } 49 | 50 | /// Generates a command to copy an SSH public key to authorized_keys. 51 | /// Properly escapes the public key to handle special characters. 52 | static String copySSHKey(String publicKey) { 53 | final escapedKey = publicKey.replaceAll("'", "\\'"); 54 | return ''' 55 | mkdir -p ~/.ssh && chmod 700 ~/.ssh && 56 | echo '$escapedKey' >> ~/.ssh/authorized_keys && 57 | chmod 600 ~/.ssh/authorized_keys 58 | '''; 59 | } 60 | 61 | Commands._(); 62 | } 63 | 64 | /// Shell command operators for chaining commands. 65 | class Operators { 66 | /// Logical AND operator. 67 | static const and = "&&"; 68 | 69 | /// Pipe operator for command chaining. 70 | static const pipe = "|"; 71 | } 72 | 73 | /// Utility class for building complex shell commands from simpler parts. 74 | /// All methods are pure functions to ensure immutability and thread safety. 75 | class CommandBuilder { 76 | /// Combines a list of commands with the AND operator (&&). 77 | /// Returns the single command if the list has only one element. 78 | static String andAll(List commands) { 79 | if (commands.isEmpty) return ''; 80 | if (commands.length == 1) return commands.first; 81 | return commands.join(' ${Operators.and} '); 82 | } 83 | 84 | /// Combines a list of commands with the pipe operator (|). 85 | /// Returns the single command if the list has only one element. 86 | static String pipeAll(List commands) { 87 | if (commands.isEmpty) return ''; 88 | if (commands.length == 1) return commands.first; 89 | return commands.join(' ${Operators.pipe} '); 90 | } 91 | 92 | /// Adds arguments to a base command. 93 | /// Returns the command with arguments appended. 94 | static String addArguments(String command, List args) { 95 | if (args.isEmpty) return command; 96 | final escapedArgs = args.map((arg) => arg.replaceAll("'", "\\'")).join(' '); 97 | return '$command $escapedArgs'; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/views/system_tools/system_tool_utils.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | // Project imports: 9 | import 'package:msm/common_widgets.dart'; 10 | import 'package:msm/constants/colors.dart'; 11 | import 'package:msm/constants/constants.dart'; 12 | import 'package:msm/providers/app_provider.dart'; 13 | import 'package:msm/ui_components/text/text.dart'; 14 | import 'package:msm/ui_components/text/textstyles.dart'; 15 | import 'package:msm/utils/commands/basic_details.dart'; 16 | import 'package:msm/utils/commands/command_executer.dart'; 17 | 18 | Future speedTestOutput(BuildContext context) { 19 | return dailogBox( 20 | onlycancel: true, 21 | context: context, 22 | title: "Speed Test", 23 | content: SizedBox(height: 130.h, child: speedTester(context))); 24 | } 25 | 26 | Widget speedTester(BuildContext context) { 27 | final AppService appService = Provider.of(context, listen: false); 28 | final bool connected = appService.connectionState; 29 | CommandExecuter commandExecuter = appService.commandExecuter; 30 | final Future speed = commandExecuter.speedTest(); 31 | if (connected) { 32 | return FutureBuilder( 33 | future: speed, 34 | builder: (context, AsyncSnapshot snapshot) { 35 | if (snapshot.connectionState == ConnectionState.done && 36 | snapshot.hasData && 37 | snapshot.data != null) { 38 | if (snapshot.data.runtimeType == Speed) { 39 | return speedData(snapshot.data!); 40 | } 41 | return AppText.text(snapshot.data!, 42 | style: AppTextStyles.regular( 43 | CommonColors.commonBlackColor, 15.sp)); 44 | } else if (snapshot.hasError) { 45 | return Center(child: serverNotConnected(appService, text: false)); 46 | } else { 47 | return commonCircularProgressIndicator; 48 | } 49 | }); 50 | } else { 51 | return Center(child: serverNotConnected(appService, text: false)); 52 | } 53 | } 54 | 55 | Widget speedData(Speed speedData) { 56 | return SingleChildScrollView( 57 | child: Column( 58 | children: [ 59 | AppText.centerSingleLineText("Download", 60 | style: AppTextStyles.regular(CommonColors.commonBlackColor, 61 | AppFontSizes.noFilesFontSize.sp)), 62 | AppText.centerSingleLineText(speedData.downloadSpeed, 63 | style: AppTextStyles.regular(CommonColors.commonBlackColor, 64 | AppFontSizes.noFilesFontSize.sp)), 65 | AppText.centerSingleLineText("Upload", 66 | style: AppTextStyles.regular(CommonColors.commonBlackColor, 67 | AppFontSizes.noFilesFontSize.sp)), 68 | AppText.centerSingleLineText(speedData.uploadSpeed, 69 | style: AppTextStyles.regular(CommonColors.commonBlackColor, 70 | AppFontSizes.noFilesFontSize.sp)), 71 | AppText.centerSingleLineText("ISP", 72 | style: AppTextStyles.regular(CommonColors.commonBlackColor, 73 | AppFontSizes.noFilesFontSize.sp)), 74 | AppText.centerSingleLineText(speedData.isp, 75 | style: AppTextStyles.regular(CommonColors.commonBlackColor, 76 | AppFontSizes.noFilesFontSize.sp)), 77 | AppText.centerSingleLineText("Ping", 78 | style: AppTextStyles.regular(CommonColors.commonBlackColor, 79 | AppFontSizes.noFilesFontSize.sp)), 80 | AppText.centerSingleLineText(speedData.ping, 81 | style: AppTextStyles.regular(CommonColors.commonBlackColor, 82 | AppFontSizes.noFilesFontSize.sp)), 83 | AppText.centerSingleLineText("Country", 84 | style: AppTextStyles.regular(CommonColors.commonBlackColor, 85 | AppFontSizes.noFilesFontSize.sp)), 86 | AppText.centerSingleLineText(speedData.country, 87 | style: AppTextStyles.regular( 88 | CommonColors.commonBlackColor, AppFontSizes.noFilesFontSize.sp)) 89 | ], 90 | ), 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /lib/router/router_utils.dart: -------------------------------------------------------------------------------- 1 | enum Pages { 2 | home, 3 | notifications, 4 | upload, 5 | commonUpload, 6 | systemTools, 7 | fileList, 8 | settings, 9 | } 10 | 11 | enum SettingsSubRoute { 12 | serverDetails, 13 | folderConfiguration, 14 | serverFunctions, 15 | appInfo, 16 | } 17 | 18 | enum SystemToolsSubRoute { 19 | liveTerminal, 20 | services, 21 | } 22 | 23 | extension AppPageExtension on Pages { 24 | String get toPath { 25 | switch (this) { 26 | case Pages.home: 27 | return "/"; 28 | case Pages.notifications: 29 | return "/notifications"; 30 | case Pages.upload: 31 | return "/upload"; 32 | case Pages.commonUpload: 33 | return "/commonUpload"; 34 | case Pages.systemTools: 35 | return "/systemTools"; 36 | case Pages.fileList: 37 | return "/fileList"; 38 | case Pages.settings: 39 | return "/settings"; 40 | } 41 | } 42 | 43 | String get toName { 44 | switch (this) { 45 | case Pages.home: 46 | return "HOME"; 47 | case Pages.notifications: 48 | return "NOTIFICATIONS"; 49 | case Pages.upload: 50 | return "UPLOAD"; 51 | case Pages.commonUpload: 52 | return "COMMON UPLOAD"; 53 | case Pages.systemTools: 54 | return "SYSTEM TOOLS"; 55 | case Pages.fileList: 56 | return "FILE LIST"; 57 | case Pages.settings: 58 | return "SETTINGS"; 59 | } 60 | } 61 | 62 | String get toTitle { 63 | switch (this) { 64 | case Pages.home: 65 | return "Home"; 66 | case Pages.notifications: 67 | return "Notifications"; 68 | case Pages.upload: 69 | return "Upload"; 70 | case Pages.commonUpload: 71 | return "Common Upload"; 72 | case Pages.systemTools: 73 | return "System Tools"; 74 | case Pages.fileList: 75 | return "File List"; 76 | case Pages.settings: 77 | return "Settings"; 78 | } 79 | } 80 | } 81 | 82 | extension SettingSubRouteExtension on SettingsSubRoute { 83 | String get toPath { 84 | switch (this) { 85 | case SettingsSubRoute.serverDetails: 86 | return "serverDetails"; 87 | case SettingsSubRoute.folderConfiguration: 88 | return "folderConfiguration"; 89 | case SettingsSubRoute.serverFunctions: 90 | return "serverFunctions"; 91 | case SettingsSubRoute.appInfo: 92 | return "appInfo"; 93 | } 94 | } 95 | 96 | String get toName { 97 | switch (this) { 98 | case SettingsSubRoute.serverDetails: 99 | return "SERVER DETAILS"; 100 | case SettingsSubRoute.folderConfiguration: 101 | return "FOLDER CONFIGURATION"; 102 | case SettingsSubRoute.serverFunctions: 103 | return "SERVER FUNCTIONS"; 104 | case SettingsSubRoute.appInfo: 105 | return "APP INFO"; 106 | } 107 | } 108 | 109 | String get toTitle { 110 | switch (this) { 111 | case SettingsSubRoute.serverDetails: 112 | return "Server Details"; 113 | case SettingsSubRoute.folderConfiguration: 114 | return "Folder Configuration"; 115 | case SettingsSubRoute.appInfo: 116 | return "App Info"; 117 | case SettingsSubRoute.serverFunctions: 118 | return "Server Functions"; 119 | } 120 | } 121 | } 122 | 123 | extension SystemToolsSubRouteExtension on SystemToolsSubRoute { 124 | String get toPath { 125 | switch (this) { 126 | case SystemToolsSubRoute.liveTerminal: 127 | return "liveTerminal"; 128 | case SystemToolsSubRoute.services: 129 | return "services"; 130 | } 131 | } 132 | 133 | String get toName { 134 | switch (this) { 135 | case SystemToolsSubRoute.liveTerminal: 136 | return "LIVE TERMINAL"; 137 | case SystemToolsSubRoute.services: 138 | return "SERVICES"; 139 | } 140 | } 141 | 142 | String get toTitle { 143 | switch (this) { 144 | case SystemToolsSubRoute.liveTerminal: 145 | return "Live Terminal"; 146 | case SystemToolsSubRoute.services: 147 | return "Services"; 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /lib/ui_components/textfield/textfield.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | // Package imports: 6 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 7 | 8 | // Project imports: 9 | import 'package:msm/constants/colors.dart'; 10 | import 'package:msm/constants/constants.dart'; 11 | import 'package:msm/ui_components/text/textstyles.dart'; 12 | import 'package:msm/ui_components/textfield/textfield_decoration.dart'; 13 | 14 | class AppTextField { 15 | static TextField simpleTextField( 16 | {required TextEditingController controller, FormFieldSetter? onChanged}) { 17 | return TextField( 18 | // controller: controller, 19 | onChanged: onChanged, 20 | style: AppTextStyles.regular( 21 | CommonColors.commonBlackColor, AppFontSizes.fileSearchFontSize.sp), 22 | decoration: AppTextFieldDecoratoion.simpleTextFieldDecoration()); 23 | } 24 | 25 | static Widget commonTextField( 26 | {required TextInputType keyboardType, 27 | required String labelText, 28 | required String hintText, 29 | FormFieldSetter? onsaved, 30 | FormFieldSetter? onChanged, 31 | List? inputFormatters, 32 | FormFieldValidator? validator, 33 | IconData? iconData, 34 | int? maxLength, 35 | String? errorText, 36 | String? initialValue, 37 | InputDecoration? decoration, 38 | bool obscureText = false, 39 | bool suffix = false, 40 | Widget? suffixIcon, 41 | bool disableLeftRightPadding = false, 42 | bool readOnly = false, 43 | TextEditingController? controller, 44 | void Function()? onSuffixIconPressed}) { 45 | double leftRightPadding = disableLeftRightPadding ? 0 : 18.w; 46 | return Padding( 47 | padding: EdgeInsets.only( 48 | top: 20.h, left: leftRightPadding, right: leftRightPadding), 49 | child: TextFormField( 50 | controller: controller, 51 | readOnly: readOnly, 52 | validator: validator, 53 | onSaved: onsaved, 54 | onChanged: onChanged, 55 | keyboardType: keyboardType, 56 | maxLength: maxLength, 57 | initialValue: initialValue, 58 | obscureText: obscureText, 59 | obscuringCharacter: "*", 60 | inputFormatters: inputFormatters, 61 | style: const TextStyle(color: CommonColors.commonBlackColor), 62 | decoration: decoration ?? 63 | InputDecoration( 64 | suffix: suffix 65 | ? InkWell( 66 | onTap: onSuffixIconPressed, 67 | child: Icon(Icons.clear, size: 14.sp), 68 | ) 69 | : null, 70 | suffixIcon: suffixIcon, 71 | contentPadding: EdgeInsets.all(20.h), 72 | labelText: labelText, 73 | hintText: hintText, 74 | errorText: errorText, 75 | hintTextDirection: TextDirection.rtl, 76 | labelStyle: 77 | const TextStyle(color: TextFormColors.inputTextColor), 78 | hintStyle: 79 | const TextStyle(color: TextFormColors.inputHintTextColor), 80 | errorStyle: 81 | const TextStyle(color: TextFormColors.inputErrorTextColor), 82 | focusedBorder: const OutlineInputBorder( 83 | borderSide: BorderSide( 84 | color: CommonColors.commonBlackColor, 85 | ), 86 | ), 87 | enabledBorder: const OutlineInputBorder( 88 | borderSide: BorderSide( 89 | color: CommonColors.commonBlackColor, 90 | ), 91 | ), 92 | errorBorder: const OutlineInputBorder( 93 | borderSide: BorderSide( 94 | color: TextFormColors.inputErrorTextColor, 95 | ), 96 | ), 97 | border: const OutlineInputBorder( 98 | borderSide: 99 | BorderSide(color: CommonColors.commonBlackColor)), 100 | )), 101 | ); 102 | } 103 | 104 | AppTextField._(); 105 | } 106 | -------------------------------------------------------------------------------- /lib/views/settings/app_info.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | import 'package:flutter_svg/flutter_svg.dart'; 7 | import 'package:package_info_plus/package_info_plus.dart'; 8 | import 'package:url_launcher/url_launcher.dart'; 9 | 10 | // Project imports: 11 | import 'package:msm/common_widgets.dart'; 12 | import 'package:msm/constants/colors.dart'; 13 | import 'package:msm/constants/constants.dart'; 14 | import 'package:msm/router/router_utils.dart'; 15 | import 'package:msm/ui_components/text/text.dart'; 16 | import 'package:msm/ui_components/text/textstyles.dart'; 17 | import 'package:msm/views/settings/settings_utils.dart'; 18 | 19 | class AppInfo extends StatelessWidget { 20 | const AppInfo({super.key}); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return appDetails(context); 25 | } 26 | } 27 | 28 | Widget appDetails(BuildContext context) { 29 | return Scaffold( 30 | appBar: commonAppBar( 31 | backroute: Pages.settings.toPath, 32 | context: context, 33 | text: SettingsSubRoute.appInfo.toTitle), 34 | backgroundColor: CommonColors.commonWhiteColor, 35 | body: Center( 36 | child: FutureBuilder( 37 | future: appInfo, 38 | builder: (BuildContext context, AsyncSnapshot snapshot) { 39 | return Column( 40 | mainAxisAlignment: MainAxisAlignment.spaceAround, 41 | children: [ 42 | Column( 43 | children: snapshot.hasData 44 | ? dataWidget(data: snapshot.data) 45 | : snapshot.hasError 46 | ? errorWidget 47 | : loadingWidget, 48 | ), 49 | links 50 | ], 51 | ); 52 | }, 53 | ), 54 | )); 55 | } 56 | 57 | List dataWidget({required PackageInfo? data}) { 58 | return [ 59 | SvgPicture.asset( 60 | AppConstants.appIconImageLocation, 61 | height: AppMeasurements.appInfoIconHeight.h, 62 | width: AppMeasurements.appInfoIconWidth.w, 63 | ), 64 | AppText.centerSingleLineText(data!.appName.toUpperCase(), 65 | style: AppTextStyles.medium(CommonColors.commonBlackColor, 66 | AppFontSizes.appShortNameFontSize.sp)), 67 | AppText.centerSingleLineText(AppConstants.appFullName, 68 | style: AppTextStyles.medium(CommonColors.commonBlackColor, 69 | AppFontSizes.appLongNameFontSize.sp)), 70 | AppText.centerSingleLineText(data.version, 71 | style: AppTextStyles.medium(CommonColors.commonBlackColor, 72 | AppFontSizes.appLongNameFontSize.sp)), 73 | ]; 74 | } 75 | 76 | Widget get links => Wrap( 77 | crossAxisAlignment: WrapCrossAlignment.center, 78 | spacing: 1.5, 79 | direction: Axis.vertical, 80 | children: [ 81 | InkWell( 82 | child: AppText.centerSingleLineText(AppConstants.homePage, 83 | style: linkStyle), 84 | onTap: () => launchUrl(Uri.parse(AppConstants.homePageUrl))), 85 | InkWell( 86 | child: AppText.centerSingleLineText(AppConstants.license, 87 | style: linkStyle), 88 | onTap: () => launchUrl(Uri.parse(AppConstants.licenseUrl))), 89 | InkWell( 90 | child: AppText.centerSingleLineText( 91 | AppConstants.appIssueFeatureReport, 92 | style: linkStyle), 93 | onTap: () => launchUrl(Uri.parse(AppConstants.issueReportUrl))) 94 | ]); 95 | 96 | List get errorWidget => [ 97 | Icon( 98 | Icons.error_outline, 99 | color: Colors.red, 100 | size: 60.sp, 101 | ), 102 | Padding( 103 | padding: EdgeInsets.only(top: 16.h), 104 | child: const Text('Error!'), 105 | ), 106 | ]; 107 | 108 | List get loadingWidget => [ 109 | SizedBox( 110 | width: 60.w, 111 | height: 60.h, 112 | child: commonCircularProgressIndicator, 113 | ), 114 | ]; 115 | 116 | TextStyle get linkStyle => AppTextStyles.medium( 117 | CommonColors.commonLinkColor, AppFontSizes.appInfoLinkFontSize.sp); 118 | -------------------------------------------------------------------------------- /lib/router/router.dart: -------------------------------------------------------------------------------- 1 | // Package imports: 2 | import 'package:go_router/go_router.dart'; 3 | 4 | import 'package:msm/providers/app_provider.dart'; 5 | import 'package:msm/router/router_utils.dart'; 6 | // Project imports: 7 | import 'package:msm/utils/server.dart'; 8 | import 'package:msm/views/file_listing/file_listing.dart'; 9 | import 'package:msm/views/home/home.dart'; 10 | import 'package:msm/views/notifications/notifications.dart'; 11 | import 'package:msm/views/settings/app_info.dart'; 12 | import 'package:msm/views/settings/folder_configuration_view.dart'; 13 | import 'package:msm/views/settings/server_details/server_details_view.dart'; 14 | import 'package:msm/views/settings/server_functions_view.dart'; 15 | import 'package:msm/views/settings/settings.dart'; 16 | import 'package:msm/views/system_tools/live_terminal.dart'; 17 | import 'package:msm/views/system_tools/services_list.dart'; 18 | import 'package:msm/views/system_tools/system_tools.dart'; 19 | import 'package:msm/views/upload_pages/common_upload_interface.dart'; 20 | import 'package:msm/views/upload_pages/upload_menu.dart'; 21 | 22 | class AppRouter { 23 | late final AppService appService; 24 | GoRouter get router => _goRouter; 25 | 26 | AppRouter(this.appService); 27 | 28 | late final GoRouter _goRouter = GoRouter( 29 | refreshListenable: appService, 30 | initialLocation: Pages.home.toPath, 31 | routes: [ 32 | GoRoute( 33 | path: Pages.home.toPath, 34 | name: Pages.home.toName, 35 | builder: (context, state) => const HomePage(), 36 | ), 37 | GoRoute( 38 | path: Pages.notifications.toPath, 39 | name: Pages.notifications.toName, 40 | builder: (context, state) => const NotificationsPage(), 41 | ), 42 | GoRoute( 43 | path: Pages.upload.toPath, 44 | name: Pages.upload.toName, 45 | builder: (context, state) => const UploadMenuPage(), 46 | ), 47 | GoRoute( 48 | path: Pages.commonUpload.toPath, 49 | name: Pages.commonUpload.toName, 50 | builder: (context, state) => const CommonUploadPage(), 51 | ), 52 | GoRoute( 53 | path: Pages.systemTools.toPath, 54 | name: Pages.systemTools.toName, 55 | builder: (context, state) => const SystemTools(), 56 | routes: [ 57 | GoRoute( 58 | path: SystemToolsSubRoute.liveTerminal.toPath, 59 | name: SystemToolsSubRoute.liveTerminal.toName, 60 | builder: (context, state) => 61 | LiveTerminalPage(appService: appService)), 62 | GoRoute( 63 | path: SystemToolsSubRoute.services.toPath, 64 | name: SystemToolsSubRoute.services.toName, 65 | builder: (context, state) => const ServicesList()) 66 | ]), 67 | GoRoute( 68 | path: Pages.fileList.toPath, 69 | name: Pages.fileList.toName, 70 | builder: (context, state) => const FileListing(), 71 | ), 72 | GoRoute( 73 | path: Pages.settings.toPath, 74 | name: Pages.settings.toName, 75 | builder: (context, state) => const Settings(), 76 | routes: [ 77 | GoRoute( 78 | path: SettingsSubRoute.serverDetails.toPath, 79 | name: SettingsSubRoute.serverDetails.toName, 80 | builder: (context, state) => const ServerDetails(), 81 | ), 82 | GoRoute( 83 | path: SettingsSubRoute.folderConfiguration.toPath, 84 | name: SettingsSubRoute.folderConfiguration.toName, 85 | builder: (context, state) => const FolderConfigurationForm(), 86 | ), 87 | GoRoute( 88 | path: SettingsSubRoute.serverFunctions.toPath, 89 | name: SettingsSubRoute.serverFunctions.toName, 90 | builder: (context, state) => const ServerFunctions(), 91 | ), 92 | GoRoute( 93 | path: SettingsSubRoute.appInfo.toPath, 94 | name: SettingsSubRoute.appInfo.toName, 95 | builder: (context, state) => const AppInfo(), 96 | ), 97 | ]) 98 | ], 99 | redirect: (context, state) async { 100 | if (!appService.server.serverData.detailsAvailable) { 101 | return "${Pages.settings.toPath}/${SettingsSubRoute.serverDetails.toPath}"; 102 | } 103 | if (appService.commandExecuter.client == null || 104 | appService.commandExecuter.client!.isClosed) { 105 | appService.server.state = ServerState.disconnected; 106 | return null; 107 | } 108 | return null; 109 | }, 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /lib/utils/background_tasks.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | // ignore_for_file: depend_on_referenced_packages 3 | 4 | // Dart imports: 5 | import 'dart:ui'; 6 | 7 | // Flutter imports: 8 | import 'package:flutter/widgets.dart'; 9 | 10 | // Package imports: 11 | import 'package:dartssh2/dartssh2.dart'; 12 | import 'package:flutter_background_service/flutter_background_service.dart'; 13 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 14 | 15 | // Project imports: 16 | import 'package:msm/constants/constants.dart'; 17 | import 'package:msm/initialization.dart'; 18 | import 'package:msm/utils/local_notification.dart'; 19 | import 'package:msm/utils/upload_and_download.dart'; 20 | import 'package:msm/providers/app_provider.dart'; 21 | 22 | @pragma('vm:entry-point') 23 | void backGroundTaskDispatcher(ServiceInstance service) async { 24 | DartPluginRegistrant.ensureInitialized(); 25 | final FlutterLocalNotificationsPlugin backgroundNotifications = 26 | FlutterLocalNotificationsPlugin(); 27 | Notifications localNotifications = Notifications( 28 | flutterLocalNotificationsPlugin: await Init.notificationInitialize()); 29 | try { 30 | if (service is AndroidServiceInstance) { 31 | if (await service.isForegroundService()) { 32 | backgroundNotifications.show( 33 | BackGroundTaskRelated.foregroundServiceNotificationId, 34 | BackGroundTaskRelated.initialNotificationTitle, 35 | BackGroundTaskRelated.runningBody, 36 | const NotificationDetails( 37 | android: AndroidNotificationDetails( 38 | BackGroundTaskRelated.notificationChannelId, 39 | BackGroundTaskRelated.initialNotificationTitle, 40 | icon: BackGroundTaskRelated.icon, 41 | playSound: false, 42 | ongoing: true, 43 | actions: [ 44 | AndroidNotificationAction(BackGroundTaskRelated.stopActionId, 45 | BackGroundTaskRelated.stopActionTitle), 46 | ], 47 | ), 48 | ), 49 | ); 50 | } 51 | service.on(Task.upload.uniqueName).listen((event) async { 52 | if (event != null) { 53 | final SftpClient sftpClient = await getSFTPClient(event); 54 | upload( 55 | newFolders: event["newFolders"].cast(), 56 | insidePath: event["insidePath"], 57 | directory: event["directory"], 58 | filePaths: event["filePaths"].cast(), 59 | notifications: localNotifications, 60 | sftp: sftpClient); 61 | } 62 | }); 63 | service.on(Task.download.uniqueName).listen((event) async { 64 | if (event != null) { 65 | final SftpClient sftpClient = await getSFTPClient(event); 66 | download( 67 | notifications: localNotifications, 68 | sftp: sftpClient, 69 | fullPath: event["fullPath"], 70 | name: event["name"]); 71 | } 72 | }); 73 | service.on('stopService').listen((event) { 74 | service.stopSelf(); 75 | }); 76 | } 77 | } catch (_) {} 78 | } 79 | 80 | enum Task { upload, download, update, cleanServer } 81 | 82 | extension TasksExtension on Task { 83 | String get uniqueName { 84 | switch (this) { 85 | case Task.upload: 86 | return BackgroundTaskUniqueNames.upload; 87 | case Task.update: 88 | return BackgroundTaskUniqueNames.update; 89 | case Task.cleanServer: 90 | return BackgroundTaskUniqueNames.cleanServer; 91 | case Task.download: 92 | return BackgroundTaskUniqueNames.download; 93 | } 94 | } 95 | } 96 | 97 | class BackgroundTasks { 98 | BackgroundTasks() { 99 | WidgetsFlutterBinding.ensureInitialized(); 100 | } 101 | 102 | void task( 103 | {required Task task, 104 | required Map data, 105 | required AppService appService}) async { 106 | data.addAll(appService.server.serverData.toJson()); 107 | final service = appService.backgroundService; 108 | bool isRunning = await service.isRunning(); 109 | if (!isRunning) { 110 | service.startService(); 111 | } 112 | switch (task) { 113 | case Task.upload: 114 | service.invoke(Task.upload.uniqueName, data); 115 | break; 116 | case Task.download: 117 | service.invoke(Task.download.uniqueName, data); 118 | break; 119 | case Task.update: 120 | // TODO: Handle this case. 121 | break; 122 | case Task.cleanServer: 123 | // TODO: Handle this case. 124 | break; 125 | } 126 | } 127 | 128 | static void start() async { 129 | final service = FlutterBackgroundService(); 130 | bool isRunning = await service.isRunning(); 131 | if (!isRunning) { 132 | service.startService(); 133 | } 134 | } 135 | 136 | static void cancel() async { 137 | final FlutterBackgroundService service = FlutterBackgroundService(); 138 | bool isRunning = await service.isRunning(); 139 | if (isRunning) { 140 | service.invoke("stopService"); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lib/views/file_listing/file_tile.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | // Project imports: 10 | import 'package:msm/common_widgets.dart'; 11 | import 'package:msm/constants/colors.dart'; 12 | import 'package:msm/constants/constants.dart'; 13 | import 'package:msm/providers/file_listing_provider.dart'; 14 | import 'package:msm/ui_components/text/text.dart'; 15 | import 'package:msm/ui_components/text/textstyles.dart'; 16 | import 'package:msm/utils/file_manager.dart'; 17 | import 'package:msm/views/file_listing/file_listing_utils.dart'; 18 | 19 | // ignore: must_be_immutable 20 | class FileTile extends StatefulWidget { 21 | final FileOrDirectory fileOrDirectory; 22 | bool selected; 23 | FileTile({super.key, required this.fileOrDirectory, required this.selected}); 24 | 25 | @override 26 | FileTileState createState() => FileTileState(); 27 | } 28 | 29 | class FileTileState extends State { 30 | bool showTile = true; 31 | bool updated = false; 32 | final TextEditingController controller = TextEditingController(); 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | FileListingState listingState = Provider.of(context); 37 | final reNameFormKey = GlobalKey(); 38 | !updated ? controller.text = widget.fileOrDirectory.name : updated = false; 39 | return Visibility( 40 | visible: showTile, 41 | child: SizedBox( 42 | height: 55.h, 43 | child: ListTile( 44 | onLongPress: (() { 45 | setState(() { 46 | listingState.selectOrRemoveItems(widget.fileOrDirectory); 47 | widget.selected = !widget.selected; 48 | }); 49 | }), 50 | onTap: () { 51 | if (listingState.fabOpen) { 52 | listingState.fabGestureDetector.onTap!(); 53 | } 54 | if (widget.selected) { 55 | setState(() { 56 | widget.selected = !widget.selected; 57 | listingState.selectOrRemoveItems(widget.fileOrDirectory); 58 | }); 59 | } else { 60 | if (!widget.fileOrDirectory.isFile) { 61 | listingState.setSearchMode = false; 62 | listingState.clearSearchText; 63 | listingState.addPath = listingState.setNextPage = 64 | "${widget.fileOrDirectory.location}/${widget.fileOrDirectory.name}"; 65 | } 66 | } 67 | }, 68 | selected: widget.selected, 69 | dense: true, 70 | visualDensity: const VisualDensity(horizontal: -4.0, vertical: -2), 71 | horizontalTitleGap: 20, 72 | leading: widget.fileOrDirectory.isFile 73 | ? widget.fileOrDirectory.category!.categoryIcon(widget.selected) 74 | : leadingIcon(FontAwesomeIcons.folder, widget.selected), 75 | title: AppText.singleLineText(controller.text, 76 | style: AppTextStyles.medium( 77 | widget.selected 78 | ? CommonColors.commonGreenColor 79 | : CommonColors.commonBlackColor, 80 | AppFontSizes.fileListTitleFontSize.sp)), 81 | subtitle: AppText.text(generateSubtitle(widget.fileOrDirectory), 82 | style: AppTextStyles.regular( 83 | widget.selected 84 | ? CommonColors.commonGreenColor 85 | : CommonColors.commonBlackColor, 86 | AppFontSizes.fileListSubtitleFontSize.sp)), 87 | trailing: commonPopUpMenu( 88 | disabledItem: !FileManager.allowedDocumentExtensions 89 | .contains(widget.fileOrDirectory.extension) 90 | ? FileActionMenu.sendKindle 91 | : null, 92 | onSelected: (selectedMenu) { 93 | selectedMenu.executeAction(widget.fileOrDirectory); 94 | if (selectedMenu == FileActionMenu.delete) { 95 | deleteSingleFile(context, widget.fileOrDirectory, 96 | extraFunctionCallback: () { 97 | setState(() { 98 | showTile = false; 99 | }); 100 | }); 101 | } 102 | if (selectedMenu == FileActionMenu.rename) { 103 | renameFile(context, widget.fileOrDirectory, reNameFormKey, 104 | renameField: reNameField( 105 | key: reNameFormKey, 106 | context: context, 107 | fileOrDirectory: widget.fileOrDirectory, 108 | controller: controller, 109 | extraFunctionCallback: () { 110 | setState(() { 111 | updated = true; 112 | }); 113 | })); 114 | } 115 | }, 116 | menuListValues: FileActionMenu.values, 117 | size: AppFontSizes.fileMenuIconSize), 118 | ), 119 | ), 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/utils/local_notification.dart: -------------------------------------------------------------------------------- 1 | // Package imports: 2 | import 'package:filesize/filesize.dart'; 3 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 4 | 5 | // Project imports: 6 | import 'package:msm/constants/constants.dart'; 7 | import 'package:msm/utils/background_tasks.dart'; 8 | 9 | @pragma('vm:entry-point') 10 | void notificationTapBackground(NotificationResponse notificationResponse) { 11 | if (notificationResponse.actionId == BackGroundTaskRelated.stopActionId) { 12 | BackgroundTasks.cancel(); 13 | } 14 | } 15 | 16 | enum NotificationType { 17 | upload, 18 | download, 19 | kindle, 20 | update; 21 | 22 | String get getString { 23 | switch (this) { 24 | case NotificationType.upload: 25 | return "Upload"; 26 | case NotificationType.download: 27 | return "Download"; 28 | case NotificationType.kindle: 29 | return "Kindle"; 30 | case NotificationType.update: 31 | return "Update"; 32 | } 33 | } 34 | } 35 | 36 | class Notifications { 37 | late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; 38 | Notifications({required this.flutterLocalNotificationsPlugin}); 39 | 40 | String _status(int? total, int? progress, NotificationType notificationType) { 41 | switch (notificationType) { 42 | case NotificationType.upload: 43 | if (total == progress) { 44 | return "Completed"; 45 | } 46 | return "Started"; 47 | case NotificationType.download: 48 | if (total == progress) { 49 | return "Completed"; 50 | } 51 | return "Started"; 52 | case NotificationType.kindle: 53 | if (total == progress) { 54 | return "Sent"; 55 | } 56 | return "Sending"; 57 | case NotificationType.update: 58 | return "Updated"; 59 | } 60 | } 61 | 62 | Future uploadNotification( 63 | {required String name, 64 | required String location, 65 | required int progress, 66 | required int fileSize, 67 | required NotificationType notificationType}) async { 68 | AndroidNotificationDetails androidNotificationDetails = 69 | AndroidNotificationDetails( 70 | BackGroundTaskRelated.uploadChannelId, 71 | BackGroundTaskRelated.uploadChannelName, 72 | importance: Importance.max, 73 | priority: Priority.high, 74 | playSound: true, 75 | channelShowBadge: false, 76 | enableVibration: true, 77 | onlyAlertOnce: true, 78 | showProgress: true, 79 | maxProgress: fileSize, 80 | progress: progress, 81 | ); 82 | NotificationDetails notificationDetails = 83 | NotificationDetails(android: androidNotificationDetails); 84 | await flutterLocalNotificationsPlugin.show( 85 | name.hashCode, 86 | '${notificationType.getString} ${_status(fileSize, progress, notificationType)} For $name', 87 | 'Saving to $location \n ${filesize(progress)}/${filesize(fileSize)}', 88 | notificationDetails); 89 | } 90 | 91 | Future sendToKindle( 92 | {required String id, 93 | required String name, 94 | required int progress, 95 | required int total, 96 | required NotificationType notificationType}) async { 97 | AndroidNotificationDetails androidNotificationDetails = 98 | AndroidNotificationDetails( 99 | id, 100 | 'upload notification', 101 | importance: Importance.max, 102 | priority: Priority.high, 103 | playSound: true, 104 | channelShowBadge: false, 105 | enableVibration: true, 106 | onlyAlertOnce: true, 107 | showProgress: true, 108 | maxProgress: total, 109 | progress: progress, 110 | ); 111 | NotificationDetails notificationDetails = 112 | NotificationDetails(android: androidNotificationDetails); 113 | await flutterLocalNotificationsPlugin.show( 114 | name.hashCode, 115 | name, 116 | 'Successfully ${_status(total, progress, notificationType)} To ${notificationType.getString} \n ${filesize(progress)}/${filesize(total)}', 117 | notificationDetails); 118 | } 119 | 120 | Future systemUpdate( 121 | {required String id, 122 | required String name, 123 | required NotificationType notificationType}) async { 124 | AndroidNotificationDetails androidNotificationDetails = 125 | AndroidNotificationDetails( 126 | id, 127 | 'system update notification', 128 | importance: Importance.max, 129 | priority: Priority.high, 130 | playSound: true, 131 | channelShowBadge: false, 132 | enableVibration: true, 133 | onlyAlertOnce: true, 134 | showProgress: false, 135 | ); 136 | NotificationDetails notificationDetails = 137 | NotificationDetails(android: androidNotificationDetails); 138 | await flutterLocalNotificationsPlugin.show( 139 | name.hashCode, 140 | name, 141 | 'Successfully ${_status(null, null, notificationType)} System', 142 | notificationDetails); 143 | } 144 | 145 | Future uploadError({required String error}) async { 146 | AndroidNotificationDetails androidNotificationDetails = 147 | const AndroidNotificationDetails( 148 | "upload error", 'upload error notification', 149 | importance: Importance.high, 150 | priority: Priority.high, 151 | playSound: true, 152 | enableVibration: true, 153 | actions: []); 154 | NotificationDetails notificationDetails = 155 | NotificationDetails(android: androidNotificationDetails); 156 | await flutterLocalNotificationsPlugin.show( 157 | error.hashCode, 'Error 😞', 'Error: $error', notificationDetails, 158 | payload: 'item x'); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lib/utils/upload_and_download.dart: -------------------------------------------------------------------------------- 1 | // Dart imports: 2 | import 'dart:io'; 3 | 4 | // Package imports: 5 | import 'package:dartssh2/dartssh2.dart'; 6 | 7 | // Project imports: 8 | import 'package:msm/constants/constants.dart'; 9 | import 'package:msm/utils/file_manager.dart'; 10 | import 'package:msm/utils/local_notification.dart'; 11 | 12 | Future getSFTPClient(dynamic event) async { 13 | SSHClient client = SSHClient( 14 | await SSHSocket.connect( 15 | event["serverHost"], 16 | int.parse(event["portNumber"]), 17 | timeout: const Duration(seconds: 10), 18 | ), 19 | username: event["username"].trim(), 20 | onPasswordRequest: () => event["rootPassword"], 21 | ); 22 | final SftpClient sftpClient = await client.sftp(); 23 | return sftpClient; 24 | } 25 | 26 | Future upload( 27 | {List newFolders = const [], 28 | String insidePath = "", 29 | required String directory, 30 | required List filePaths, 31 | required SftpClient? sftp, 32 | required Notifications? notifications}) async { 33 | if (filePaths.isNotEmpty) { 34 | if (insidePath.isNotEmpty) { 35 | directory = "$directory/$insidePath"; 36 | } 37 | if (newFolders.isEmpty) { 38 | loopAndSend( 39 | filePaths: filePaths, 40 | directory: directory, 41 | notifications: notifications, 42 | sftp: sftp); 43 | } else { 44 | await _createFolders( 45 | directory: directory, 46 | newFolders: newFolders, 47 | sftp: sftp, 48 | notifications: notifications) 49 | .then((createdDirectoryPath) async { 50 | loopAndSend( 51 | filePaths: filePaths, 52 | directory: createdDirectoryPath, 53 | notifications: notifications, 54 | sftp: sftp); 55 | }); 56 | } 57 | } else { 58 | notifications!.uploadError(error: AppMessages.filesNotSelected); 59 | } 60 | } 61 | 62 | Future _createFolders( 63 | {required String directory, 64 | required List newFolders, 65 | required SftpClient? sftp, 66 | required Notifications? notifications}) async { 67 | try { 68 | for (String folder in newFolders) { 69 | directory += "/$folder"; 70 | await sftp!.mkdir(directory); 71 | } 72 | return directory; 73 | } catch (_) { 74 | notifications!.uploadError(error: AppMessages.folderCreationError); 75 | return AppMessages.folderCreationError; 76 | } 77 | } 78 | 79 | Future notify( 80 | {required String fileName, 81 | required String location, 82 | required int fileSize, 83 | required int progress, 84 | required Notifications? notifications, 85 | required NotificationType notificationType}) async { 86 | await notifications!.uploadNotification( 87 | name: fileName, 88 | location: location, 89 | progress: progress, 90 | fileSize: fileSize, 91 | notificationType: notificationType); 92 | } 93 | 94 | void loopAndSend( 95 | {required List filePaths, 96 | required String directory, 97 | required SftpClient? sftp, 98 | required Notifications? notifications}) { 99 | for (String filePath in filePaths) { 100 | sendFile( 101 | directory: directory, 102 | filePath: filePath, 103 | sftp: sftp, 104 | notifications: notifications); 105 | } 106 | } 107 | 108 | Future sendFile( 109 | {required String directory, 110 | required String filePath, 111 | required SftpClient? sftp, 112 | required Notifications? notifications}) async { 113 | try { 114 | if (sftp != null) { 115 | final int totalFileSize = File(filePath).lengthSync(); 116 | final String fileName = filePath.split('/').last.toString(); 117 | final String remotePath = "$directory/$fileName"; 118 | final remoteFile = await sftp.open(remotePath, 119 | mode: SftpFileOpenMode.create | SftpFileOpenMode.write); 120 | await remoteFile.write( 121 | File(filePath).openRead().cast(), 122 | onProgress: (progress) => notify( 123 | fileName: fileName, 124 | location: directory, 125 | fileSize: totalFileSize, 126 | progress: progress, 127 | notificationType: NotificationType.upload, 128 | notifications: notifications), 129 | ); 130 | } else { 131 | notifications!.uploadError(error: AppMessages.serverNotAvailable); 132 | } 133 | } catch (e) { 134 | notifications!.uploadError(error: e.toString()); 135 | } 136 | } 137 | 138 | Future download( 139 | {required SftpClient? sftp, 140 | required Notifications? notifications, 141 | required String fullPath, 142 | required String name}) async { 143 | try { 144 | final remoteFile = await sftp!.open(fullPath); 145 | File localFileObj = File("${FileManager.downloadLocation}/$name"); 146 | final size = (await remoteFile.stat()).size; 147 | const defaultChunkSize = 1024 * 1024 * 10; //10MB 148 | if (size != null) { 149 | int chunkSize = size > defaultChunkSize ? defaultChunkSize : size; 150 | for (var i = chunkSize; chunkSize > 0; i += chunkSize) { 151 | final fileData = await remoteFile.readBytes( 152 | length: chunkSize, offset: i - chunkSize); 153 | await localFileObj.writeAsBytes(fileData, 154 | mode: FileMode.append, flush: true); 155 | notify( 156 | fileName: name, 157 | location: FileManager.downloadLocation, 158 | fileSize: size, 159 | progress: i, 160 | notificationType: NotificationType.download, 161 | notifications: notifications); 162 | if (i + chunkSize > size) { 163 | chunkSize = size - i; 164 | } 165 | } 166 | } 167 | } catch (_) {} 168 | } 169 | -------------------------------------------------------------------------------- /lib/views/home/home.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | import 'package:msm/common_widgets.dart'; 9 | import 'package:msm/constants/colors.dart'; 10 | import 'package:msm/constants/constants.dart'; 11 | import 'package:msm/providers/app_provider.dart'; 12 | import 'package:msm/ui_components/text/text.dart'; 13 | import 'package:msm/ui_components/text/textstyles.dart'; 14 | // Project imports: 15 | import 'package:msm/utils.dart'; 16 | import 'package:msm/utils/commands/basic_details.dart'; 17 | import 'package:msm/utils/commands/command_executer.dart'; 18 | import 'package:msm/views/home/home_common_widgets.dart'; 19 | import 'package:msm/views/home/home_utils.dart'; 20 | import 'package:msm/views/home/real_time_basic_details.dart'; 21 | 22 | class HomePage extends StatelessWidget { 23 | const HomePage({super.key}); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return handleBackButton(child: home(context), context: context); 28 | } 29 | } 30 | 31 | Widget home(BuildContext context) { 32 | return GestureDetector( 33 | // onHorizontalDragUpdate: (details) => notificationsPage(context, details), 34 | child: Scaffold( 35 | backgroundColor: CommonColors.commonWhiteColor, 36 | body: SingleChildScrollView( 37 | child: Column( 38 | children: [ 39 | menuGrid(context), 40 | serverDetailsBuilder(context), 41 | ], 42 | ), 43 | )), 44 | ); 45 | } 46 | 47 | Widget menuGrid(BuildContext context) { 48 | return Padding( 49 | padding: EdgeInsets.only(top: 18.h, bottom: 30.h), 50 | child: GridView.count( 51 | shrinkWrap: true, 52 | padding: EdgeInsets.all(16.h), 53 | crossAxisCount: 2, 54 | children: List.generate(4, (index) { 55 | return Container( 56 | color: CommonColors.commonGreenColor, 57 | margin: EdgeInsets.all(8.h), 58 | child: OutlinedButton( 59 | onPressed: () => goToPage(index, context), 60 | child: Center( 61 | child: homeIconList[index], 62 | )), 63 | ); 64 | }), 65 | ), 66 | ); 67 | } 68 | 69 | Widget serverDetailsBuilder(BuildContext context) { 70 | final AppService appService = Provider.of(context); 71 | final bool connected = appService.connectionState; 72 | if (connected) { 73 | CommandExecuter commandExecuter = appService.commandExecuter; 74 | final Future basicDetails = commandExecuter.basicDetails; 75 | return FutureBuilder( 76 | future: basicDetails, 77 | builder: (BuildContext context, AsyncSnapshot snapshot) { 78 | if (snapshot.connectionState == ConnectionState.done && 79 | snapshot.hasData) { 80 | return serverdetails(snapshot.data, appService); 81 | } else { 82 | return fetchingData; 83 | } 84 | }); 85 | } 86 | return serverNotConnected(appService); 87 | } 88 | 89 | class ServerDetailsWidget extends StatelessWidget { 90 | final BasicDetails? data; 91 | final AppService appService; 92 | 93 | const ServerDetailsWidget({ 94 | super.key, 95 | required this.data, 96 | required this.appService, 97 | }); 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | // Validate input data 102 | if (data == null) { 103 | return const SizedBox.shrink(); 104 | } 105 | 106 | // Ensure disk usage percentage is within valid range 107 | final double diskUsage = data!.diskUsagePercentage.clamp(0.0, 1.0); 108 | 109 | return Stack( 110 | children: [ 111 | // Positioned widget for real-time basic details overlay 112 | Positioned( 113 | top: AppMeasurements.realTimeDetailsTopPosition.h, 114 | left: AppMeasurements.realTimeDetailsHorizontalMargin.w, 115 | right: AppMeasurements.realTimeDetailsHorizontalMargin.w, 116 | child: RealTimeBasicDetails( 117 | appService: appService, 118 | basicDetails: data!, 119 | ), 120 | ), 121 | // Centered circular progress indicator for disk usage 122 | Center( 123 | child: SizedBox( 124 | width: AppMeasurements.diskUsageIndicatorSize.w, 125 | height: AppMeasurements.diskUsageIndicatorSize.w, 126 | child: TweenAnimationBuilder( 127 | tween: Tween(begin: 0.0, end: diskUsage), 128 | duration: AppDurations.diskUsageAnimation, 129 | curve: Curves.easeOut, 130 | builder: (context, double value, _) => CircularProgressIndicator( 131 | strokeWidth: AppMeasurements.diskUsageIndicatorStrokeWidth.sp, 132 | color: CommonColors.commonGreenColor, 133 | backgroundColor: CommonColors.diskUsageBackgroundColor, 134 | value: value, 135 | semanticsLabel: 'System Disk Usage Data', 136 | ), 137 | ), 138 | ), 139 | ), 140 | ], 141 | ); 142 | } 143 | } 144 | 145 | // Legacy function for backward compatibility 146 | Widget serverdetails(BasicDetails data, AppService appService) => 147 | ServerDetailsWidget(data: data, appService: appService); 148 | 149 | Widget get fetchingData => Column( 150 | children: [ 151 | Padding( 152 | padding: EdgeInsets.all(20.h), 153 | child: commonCircularProgressIndicator), 154 | AppText.centerSingleLineText(AppConstants.connecting, 155 | style: AppTextStyles.regular(CommonColors.commonBlackColor, 156 | AppFontSizes.connectingFontSize)), 157 | ], 158 | ); 159 | -------------------------------------------------------------------------------- /shell_scripts/basic_details.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # ----------------------------- 4 | # Linux System Information 5 | # ----------------------------- 6 | 7 | # Username 8 | username=$(whoami) 9 | 10 | # Uptime (pretty format) 11 | uptime=$(uptime -p | sed "s/up //") 12 | 13 | # Temperature (sensors → fallback to thermal_zone) 14 | temp=$(sensors 2>/dev/null | awk '/Package id 0/ {print $4}' | head -n1) 15 | if [ -z "$temp" ]; then 16 | temp=$(awk '{print $1/1000"°C"}' /sys/class/thermal/thermal_zone*/temp 2>/dev/null | head -n1) 17 | fi 18 | 19 | # Memory (human readable) 20 | read mem_total mem_used mem_free <<<$(free -h | awk 'NR==2 {print $2, $3, $7}') 21 | 22 | # ----------------------------- 23 | # Disk information (internal only) 24 | # ----------------------------- 25 | disk_json="" 26 | 27 | while read src size used avail pcent target; do 28 | echo "$src" | grep -q "^/dev/" || continue 29 | 30 | parent=$(lsblk -no PKNAME "$src" 2>/dev/null) 31 | [ -z "$parent" ] && parent=$(basename "$src") 32 | 33 | tran=$(lsblk -dno TRAN "/dev/$parent" 2>/dev/null) 34 | rm=$(lsblk -dno RM "/dev/$parent" 2>/dev/null) 35 | fstype=$(lsblk -no FSTYPE "$src" 2>/dev/null) 36 | 37 | # Skip USB drives — keep internal disks only 38 | if [ "$tran" != "usb" ]; then 39 | entry="{\"filesystem\":\"$src\",\"size\":\"$size\",\"used\":\"$used\",\"avail\":\"$avail\",\"use%\":\"$pcent\",\"mount\":\"$target\",\"fstype\":\"$fstype\",\"device\":\"$parent\",\"transport\":\"$tran\",\"removable\":$rm}" 40 | 41 | if [ -z "$disk_json" ]; then 42 | disk_json="$entry" 43 | else 44 | disk_json="$disk_json,$entry" 45 | fi 46 | fi 47 | 48 | done < <(df -hl --exclude-type=overlay --output=source,size,used,avail,pcent,target | tail -n +2) 49 | 50 | 51 | # ----------------- CPU METRICS ----------------- 52 | 53 | # CPU model 54 | cpu_model=$(awk -F: '/model name/ {print $2; exit}' /proc/cpuinfo | sed 's/^ //') 55 | 56 | # CPU cores 57 | cpu_cores=$(nproc --all) 58 | 59 | # Bogomips (rough CPU score) 60 | cpu_bogomips=$(awk -F: '/bogomips/ {print $2; exit}' /proc/cpuinfo | sed 's/^ //') 61 | 62 | # Current CPU frequency (kHz → MHz) 63 | cpu_freq=$(awk '{printf "%.0f", $1/1000}' /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 2>/dev/null) 64 | 65 | # CPU usage (sample twice) 66 | read cpu_user1 cpu_nice1 cpu_system1 cpu_idle1 cpu_iow1 cpu_irq1 cpu_sirq1 <<<$(awk '/^cpu / {print $2,$3,$4,$5,$6,$7,$8}' /proc/stat) 67 | sleep 1 68 | read cpu_user2 cpu_nice2 cpu_system2 cpu_idle2 cpu_iow2 cpu_irq2 cpu_sirq2 <<<$(awk '/^cpu / {print $2,$3,$4,$5,$6,$7,$8}' /proc/stat) 69 | 70 | cpu_delta=$(( (cpu_user2+cpu_nice2+cpu_system2+cpu_iow2+cpu_irq2+cpu_sirq2) - (cpu_user1+cpu_nice1+cpu_system1+cpu_iow1+cpu_irq1+cpu_sirq1) )) 71 | idle_delta=$(( cpu_idle2 - cpu_idle1 )) 72 | cpu_usage=$(( 100 * (cpu_delta - idle_delta) / cpu_delta )) 73 | 74 | # 1, 5, 15 min load averages 75 | read load1 load5 load15 _ < /proc/loadavg 76 | 77 | # CPU JSON 78 | cpu_json=$(printf '{"model":"%s","cores":%s,"usage":%s,"load":[%s,%s,%s],"bogomips":"%s","freq_mhz":"%s"}' \ 79 | "$cpu_model" "$cpu_cores" "$cpu_usage" "$load1" "$load5" "$load15" "$cpu_bogomips" "$cpu_freq") 80 | 81 | 82 | 83 | # ----------------------------- 84 | # Network information (speed + ping) 85 | # ----------------------------- 86 | 87 | # Get active interface 88 | # Get active network interface with error handling 89 | get_active_interface() { 90 | local iface 91 | iface=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}') 92 | if [ -z "$iface" ] || [ ! -d "/sys/class/net/$iface" ]; then 93 | echo "No active network interface found" >&2 94 | return 1 95 | fi 96 | echo "$iface" 97 | } 98 | 99 | # Read network statistics safely 100 | read_net_stats() { 101 | local iface=$1 102 | local rx tx 103 | rx=$(cat "/sys/class/net/$iface/statistics/rx_bytes" 2>/dev/null) 104 | tx=$(cat "/sys/class/net/$iface/statistics/tx_bytes" 2>/dev/null) 105 | if [ -z "$rx" ] || [ -z "$tx" ]; then 106 | echo "Failed to read network statistics" >&2 107 | return 1 108 | fi 109 | echo "$rx $tx" 110 | } 111 | 112 | # Convert bytes per second to human readable format using awk 113 | human_speed() { 114 | local bytes_per_sec=$1 115 | if [ "$bytes_per_sec" -ge 1048576 ]; then 116 | awk -v bps="$bytes_per_sec" 'BEGIN {printf "%.2f MB/s", bps/1048576}' 117 | elif [ "$bytes_per_sec" -ge 1024 ]; then 118 | awk -v bps="$bytes_per_sec" 'BEGIN {printf "%.2f KB/s", bps/1024}' 119 | else 120 | printf "%d B/s" "$bytes_per_sec" 121 | fi 122 | } 123 | 124 | # Measure ping with validation 125 | measure_ping() { 126 | local ping_time 127 | ping_time=$(ping -c1 -W1 8.8.8.8 2>/dev/null | grep "time=" | sed 's/.*time=\([0-9.]*\).*/\1/') 128 | # Validate it's a valid number 129 | if [[ $ping_time =~ ^[0-9]+(\.[0-9]+)?$ ]]; then 130 | echo "$ping_time" 131 | else 132 | echo "0" 133 | fi 134 | } 135 | 136 | # Main network measurement logic 137 | iface=$(get_active_interface) 138 | if [ $? -ne 0 ]; then 139 | # Handle error case - set defaults 140 | down_raw=0 141 | up_raw=0 142 | ping_raw=0 143 | else 144 | # Read initial statistics 145 | read rx1 tx1 <<< $(read_net_stats "$iface") 146 | sleep 1 147 | # Read final statistics 148 | read rx2 tx2 <<< $(read_net_stats "$iface") 149 | 150 | down_raw=$((rx2 - rx1)) 151 | up_raw=$((tx2 - tx1)) 152 | 153 | 154 | ping_raw=$(measure_ping) 155 | fi 156 | 157 | 158 | # ----------------------------- 159 | # Output final JSON 160 | # ----------------------------- 161 | 162 | printf "{" 163 | printf "\"username\":\"%s\"," "$username" 164 | printf "\"uptime\":\"%s\"," "$uptime" 165 | printf "\"temperature\":\"%s\"," "$temp" 166 | printf "\"ram\":{\"available\":\"%s\",\"used\":\"%s\",\"size\":\"%s\"}," "$mem_free" "$mem_used" "$mem_total" 167 | printf "\"disk\":[%s]," "$disk_json" 168 | printf "\"network\":{" 169 | printf "\"download\":{\"value\":%s,\"unit\":\"B/s\"}," "$down_raw" 170 | printf "\"upload\":{\"value\":%s,\"unit\":\"B/s\"}," "$up_raw" 171 | printf "\"ping\":{\"value\":%s,\"unit\":\"ms\"}" "$ping_raw" 172 | printf "}," 173 | printf "\"cpu\":%s" "$cpu_json" 174 | printf "}\n" 175 | -------------------------------------------------------------------------------- /lib/views/upload_pages/common_upload_interface.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | // Package imports: 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | // Project imports: 10 | import 'package:msm/utils.dart'; 11 | import 'package:msm/common_widgets.dart'; 12 | import 'package:msm/constants/colors.dart'; 13 | import 'package:msm/constants/constants.dart'; 14 | import 'package:msm/utils/file_manager.dart'; 15 | import 'package:msm/providers/upload_provider.dart'; 16 | import 'package:msm/ui_components/text/text.dart'; 17 | import 'package:msm/ui_components/text/textstyles.dart'; 18 | import 'package:msm/views/upload_pages/upload_item_card.dart'; 19 | import 'package:msm/views/upload_pages/upload_page_utils.dart'; 20 | 21 | class CommonUploadPage extends StatefulWidget { 22 | const CommonUploadPage({super.key}); 23 | 24 | @override 25 | CommonUploadPageState createState() => CommonUploadPageState(); 26 | } 27 | 28 | class CommonUploadPageState extends State { 29 | @override 30 | Widget build(BuildContext context) { 31 | UploadState uploadState = Provider.of(context); 32 | return handleBackButton( 33 | child: commonUpload(context, uploadState), 34 | context: context, 35 | backRoute: getBackPage(uploadState), 36 | uploadState: uploadState); 37 | } 38 | } 39 | 40 | Widget commonUpload(BuildContext context, UploadState uploadState) { 41 | return Scaffold( 42 | appBar: appBar(context, uploadState), 43 | backgroundColor: CommonColors.commonWhiteColor, 44 | body: body(context, uploadState)); 45 | } 46 | 47 | Widget body(BuildContext context, UploadState uploadState) { 48 | return FutureBuilder>( 49 | future: FileManager.getAllFiles(uploadState), 50 | builder: 51 | (BuildContext context, AsyncSnapshot> snapshot) { 52 | if (snapshot.connectionState == ConnectionState.done && 53 | snapshot.hasData) { 54 | if (snapshot.data!.isEmpty) { 55 | return Center( 56 | child: AppText.centerText( 57 | "No ${uploadState.getCategory.getTitle} Here", 58 | style: AppTextStyles.bold(CommonColors.commonBlackColor, 59 | AppFontSizes.noDataFontSize.sp)), 60 | ); 61 | } else { 62 | return ListView.builder( 63 | itemCount: snapshot.data!.length, 64 | itemBuilder: (BuildContext context, int index) { 65 | return TextButton( 66 | onPressed: () { 67 | goInside(snapshot.data![index], uploadState, context); 68 | }, 69 | child: uploadItemCard( 70 | context, snapshot.data![index], uploadState), 71 | ); 72 | }); 73 | } 74 | } else if (snapshot.hasError) { 75 | return Text('Error: ${snapshot.error}'); 76 | } else { 77 | return commonCircularProgressIndicator; 78 | } 79 | }, 80 | ); 81 | } 82 | 83 | Widget uploadItemCard( 84 | BuildContext context, FileOrDirectory data, UploadState uploadState) { 85 | return Padding( 86 | padding: EdgeInsets.only(top: 10.h), 87 | child: Stack( 88 | children: [ 89 | dataCard(data), 90 | UploadItemCard(data: data, fileUploadData: uploadState.fileUploadData) 91 | ], 92 | ), 93 | ); 94 | } 95 | 96 | PreferredSizeWidget appBar(BuildContext context, UploadState uploadState) { 97 | return commonAppBar( 98 | context: context, 99 | text: uploadState.getCurrentListing, 100 | backroute: getBackPage(uploadState), 101 | actions: [ 102 | Padding( 103 | padding: EdgeInsets.all(10.h), 104 | child: IconButton( 105 | onPressed: () { 106 | uploadState.fileAddOrRemove; 107 | uploadState.fileUploadData.clear; 108 | }, 109 | icon: trashIcon), 110 | ), 111 | Padding( 112 | padding: EdgeInsets.all(10.h), 113 | child: IconButton( 114 | onPressed: (() { 115 | if (uploadState.fileUploadData.uploadData.isNotEmpty) { 116 | uploadState.commonClear; 117 | bottomSheet(context, uploadState, saveHere: true); 118 | } else { 119 | showMessage(context: context, text: AppMessages.selectFiles); 120 | } 121 | }), 122 | icon: sendIcon), 123 | ) 124 | ], 125 | uploadState: uploadState); 126 | } 127 | 128 | Widget get sendIcon => const Icon( 129 | Icons.send_outlined, 130 | color: CommonColors.commonBlackColor, 131 | size: AppFontSizes.appBarIconSize, 132 | ); 133 | 134 | Widget get trashIcon => const Icon( 135 | FontAwesomeIcons.trashCan, 136 | color: CommonColors.commonBlackColor, 137 | size: AppFontSizes.appBarIconSize, 138 | ); 139 | 140 | Widget fileName(FileOrDirectory data) { 141 | return AppText.singleLineText(data.name, 142 | style: AppTextStyles.medium(CommonColors.commonBlackColor, 15)); 143 | } 144 | 145 | Widget fileMetaData(FileOrDirectory data) { 146 | return Column( 147 | crossAxisAlignment: CrossAxisAlignment.start, 148 | children: [ 149 | AppText.text(data.isFile ? data.location.toString() : "Folder", 150 | style: AppTextStyles.medium(CommonColors.commonGreyColor, 10)), 151 | AppText.text( 152 | data.isFile 153 | ? "${data.extension}, ${data.size}" 154 | : "Files: ${data.fileCount}", 155 | style: AppTextStyles.medium(CommonColors.commonGreyColor, 10)) 156 | ], 157 | ); 158 | } 159 | 160 | Widget dataCard(FileOrDirectory data) { 161 | return Center( 162 | child: Card( 163 | elevation: 3, 164 | shape: RoundedRectangleBorder( 165 | borderRadius: BorderRadius.all(Radius.circular(12.r)), 166 | ), 167 | child: SizedBox( 168 | width: 300.w, 169 | height: 122.h, 170 | child: Padding( 171 | padding: EdgeInsets.all(15.h), 172 | child: Column( 173 | mainAxisAlignment: MainAxisAlignment.spaceAround, 174 | crossAxisAlignment: CrossAxisAlignment.start, 175 | children: [fileName(data), fileMetaData(data)]), 176 | ), 177 | ), 178 | ), 179 | ); 180 | } 181 | -------------------------------------------------------------------------------- /lib/ui_components/floating_action_button/fab.dart: -------------------------------------------------------------------------------- 1 | // Dart imports: 2 | import 'dart:math' as math; 3 | 4 | // Flutter imports: 5 | import 'package:flutter/material.dart'; 6 | 7 | // Project imports: 8 | import 'package:msm/constants/colors.dart'; 9 | import 'package:msm/providers/file_listing_provider.dart'; 10 | 11 | @immutable 12 | class ExpandableFab extends StatefulWidget { 13 | const ExpandableFab({ 14 | super.key, 15 | this.initialOpen, 16 | this.fileListingState, 17 | required this.distance, 18 | required this.children, 19 | }); 20 | 21 | final FileListingState? fileListingState; 22 | final bool? initialOpen; 23 | final double distance; 24 | final List children; 25 | 26 | @override 27 | State createState() => _ExpandableFabState(); 28 | } 29 | 30 | class _ExpandableFabState extends State 31 | with SingleTickerProviderStateMixin { 32 | late final AnimationController _controller; 33 | late final Animation _expandAnimation; 34 | bool _open = false; 35 | 36 | @override 37 | void initState() { 38 | super.initState(); 39 | _open = widget.initialOpen ?? false; 40 | _controller = AnimationController( 41 | value: _open ? 1.0 : 0.0, 42 | duration: const Duration(milliseconds: 250), 43 | vsync: this, 44 | ); 45 | GestureDetector gestureDetector = GestureDetector( 46 | onTap: _toggle, 47 | ); 48 | if (widget.fileListingState != null) { 49 | widget.fileListingState!.fabOpen = _open; 50 | widget.fileListingState!.fabGestureDetector = gestureDetector; 51 | } 52 | _expandAnimation = CurvedAnimation( 53 | curve: Curves.fastOutSlowIn, 54 | reverseCurve: Curves.easeOutQuad, 55 | parent: _controller, 56 | ); 57 | } 58 | 59 | @override 60 | void dispose() { 61 | _controller.dispose(); 62 | super.dispose(); 63 | } 64 | 65 | void _toggle() { 66 | setState(() { 67 | _open = !_open; 68 | if (widget.fileListingState != null) { 69 | widget.fileListingState!.fabOpen = _open; 70 | } 71 | if (_open) { 72 | _controller.forward(); 73 | } else { 74 | _controller.reverse(); 75 | } 76 | }); 77 | } 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | return SizedBox.expand( 82 | child: Stack( 83 | alignment: Alignment.bottomRight, 84 | clipBehavior: Clip.none, 85 | children: [ 86 | _buildTapToCloseFab(), 87 | ..._buildExpandingActionButtons(), 88 | _buildTapToOpenFab(), 89 | ], 90 | ), 91 | ); 92 | } 93 | 94 | Widget _buildTapToCloseFab() { 95 | return SizedBox( 96 | width: 56.0, 97 | height: 56.0, 98 | child: Center( 99 | child: Material( 100 | shape: const CircleBorder(), 101 | clipBehavior: Clip.antiAlias, 102 | elevation: 4.0, 103 | child: InkWell( 104 | onTap: _toggle, 105 | child: const Padding( 106 | padding: EdgeInsets.all(8.0), 107 | child: Icon( 108 | Icons.close, 109 | color: CommonColors.commonBlackColor, 110 | ), 111 | ), 112 | ), 113 | ), 114 | ), 115 | ); 116 | } 117 | 118 | List _buildExpandingActionButtons() { 119 | final children = []; 120 | final count = widget.children.length; 121 | final step = 90.0 / (count - 1); 122 | for (var i = 0, angleInDegrees = 0.0; 123 | i < count; 124 | i++, angleInDegrees += step) { 125 | children.add( 126 | _ExpandingActionButton( 127 | directionInDegrees: angleInDegrees, 128 | maxDistance: widget.distance, 129 | progress: _expandAnimation, 130 | child: widget.children[i], 131 | ), 132 | ); 133 | } 134 | return children; 135 | } 136 | 137 | Widget _buildTapToOpenFab() { 138 | return IgnorePointer( 139 | ignoring: _open, 140 | child: AnimatedContainer( 141 | transformAlignment: Alignment.center, 142 | transform: Matrix4.diagonal3Values( 143 | _open ? 0.7 : 1.0, 144 | _open ? 0.7 : 1.0, 145 | 1.0, 146 | ), 147 | duration: const Duration(milliseconds: 250), 148 | curve: const Interval(0.0, 0.5, curve: Curves.easeOut), 149 | child: AnimatedOpacity( 150 | opacity: _open ? 0.0 : 1.0, 151 | curve: const Interval(0.25, 1.0, curve: Curves.easeInOut), 152 | duration: const Duration(milliseconds: 250), 153 | child: FloatingActionButton( 154 | backgroundColor: CommonColors.commonBlackColor, 155 | onPressed: _toggle, 156 | child: const Icon(Icons.sort), 157 | ), 158 | ), 159 | ), 160 | ); 161 | } 162 | } 163 | 164 | @immutable 165 | class _ExpandingActionButton extends StatelessWidget { 166 | const _ExpandingActionButton({ 167 | required this.directionInDegrees, 168 | required this.maxDistance, 169 | required this.progress, 170 | required this.child, 171 | }); 172 | 173 | final double directionInDegrees; 174 | final double maxDistance; 175 | final Animation progress; 176 | final Widget child; 177 | 178 | @override 179 | Widget build(BuildContext context) { 180 | return AnimatedBuilder( 181 | animation: progress, 182 | builder: (context, child) { 183 | final offset = Offset.fromDirection( 184 | directionInDegrees * (math.pi / 180.0), 185 | progress.value * maxDistance, 186 | ); 187 | return Positioned( 188 | right: 4.0 + offset.dx, 189 | bottom: 4.0 + offset.dy, 190 | child: Transform.rotate( 191 | angle: (1.0 - progress.value) * math.pi / 2, 192 | child: child!, 193 | ), 194 | ); 195 | }, 196 | child: FadeTransition( 197 | opacity: progress, 198 | child: child, 199 | ), 200 | ); 201 | } 202 | } 203 | 204 | @immutable 205 | class ActionButton extends StatelessWidget { 206 | const ActionButton({ 207 | super.key, 208 | this.onPressed, 209 | required this.icon, 210 | }); 211 | 212 | final VoidCallback? onPressed; 213 | final Widget icon; 214 | 215 | @override 216 | Widget build(BuildContext context) { 217 | return Material( 218 | shape: const CircleBorder(), 219 | clipBehavior: Clip.antiAlias, 220 | color: CommonColors.commonBlackColor, 221 | elevation: 4.0, 222 | child: IconButton( 223 | onPressed: onPressed, 224 | icon: icon, 225 | color: CommonColors.commonWhiteColor, 226 | ), 227 | ); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /lib/views/home/home_common_widgets.dart: -------------------------------------------------------------------------------- 1 | // Dart imports: 2 | import 'dart:async'; 3 | 4 | // Flutter imports: 5 | import 'package:flutter/material.dart'; 6 | 7 | // Package imports: 8 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 9 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 10 | 11 | // Project imports: 12 | import 'package:msm/common_widgets.dart'; 13 | import 'package:msm/constants/colors.dart'; 14 | import 'package:msm/constants/constants.dart'; 15 | import 'package:msm/providers/app_provider.dart'; 16 | import 'package:msm/ui_components/text/text.dart'; 17 | import 'package:msm/ui_components/text/textstyles.dart'; 18 | import 'package:msm/utils/commands/basic_details.dart'; 19 | 20 | /// Data class representing an icon configuration for the home page. 21 | class HomeIconData { 22 | final IconData icon; 23 | final bool fontAwesome; 24 | final Color color; 25 | 26 | const HomeIconData(this.icon, 27 | {this.fontAwesome = false, this.color = Colors.white}); 28 | } 29 | 30 | /// Builds an icon widget for the home page. 31 | /// 32 | /// [icon] The icon data to display. 33 | /// [fontAwesome] Whether to use FontAwesome icon. Defaults to false. 34 | /// [color] The color of the icon. Defaults to white. 35 | Widget homePageIcon(IconData icon, 36 | {bool fontAwesome = false, Color color = Colors.white}) { 37 | final double iconSize = AppFontSizes.homePageIconFontSize.h; 38 | if (fontAwesome) { 39 | return FaIcon(icon, size: iconSize, color: color); 40 | } else { 41 | return Icon(icon, size: iconSize, color: color); 42 | } 43 | } 44 | 45 | /// Configuration data for home page icons. Order matters for display/navigation. 46 | const List homeIconDataList = [ 47 | HomeIconData(Icons.cloud_upload_outlined), 48 | HomeIconData(FontAwesomeIcons.screwdriverWrench, fontAwesome: true), 49 | HomeIconData(Icons.folder_outlined), 50 | HomeIconData(Icons.settings), 51 | ]; 52 | 53 | /// Computed list of home page icon widgets from the data configuration. 54 | List get homeIconList => homeIconDataList 55 | .map((data) => homePageIcon(data.icon, 56 | fontAwesome: data.fontAwesome, color: data.color)) 57 | .toList(); 58 | 59 | Widget serverStats(IconData icon, String text) { 60 | return Padding( 61 | padding: EdgeInsets.only(right: 8.w), 62 | child: Wrap( 63 | spacing: 6.0, // gap between adjacent chips 64 | children: [ 65 | Icon(icon, size: 18.h, color: CommonColors.commonBlackColor), 66 | AppText.singleLineText(text, 67 | style: AppTextStyles.bold(CommonColors.commonBlackColor, 68 | AppFontSizes.serverStatFontSize.toDouble())) 69 | ], 70 | ), 71 | ); 72 | } 73 | 74 | /// Builds the server header section with icon and user name. 75 | Widget _buildServerHeader(BasicDetails data) { 76 | return Column( 77 | crossAxisAlignment: CrossAxisAlignment.stretch, 78 | children: [ 79 | homePageIcon(Icons.cloud, color: CommonColors.commonGreenColor), 80 | Padding( 81 | padding: EdgeInsets.only(bottom: 8.h), 82 | child: AppText.centerSingleLineText( 83 | data.user, 84 | style: AppTextStyles.bold( 85 | CommonColors.commonBlackColor, 86 | AppFontSizes.serverStatFontSize, 87 | ), 88 | ), 89 | ), 90 | ], 91 | ); 92 | } 93 | 94 | /// Builds the server statistics section with RAM, disk, temperature, and uptime. 95 | Widget _buildServerStats(BasicDetails data) { 96 | return Column( 97 | children: [ 98 | serverStats( 99 | FontAwesomeIcons.brain, 100 | data.cpu.formattedModelName, 101 | ), 102 | SizedBox(height: AppMeasurements.serverStatGap.h), 103 | serverStats( 104 | FontAwesomeIcons.microchip, 105 | '${data.cpu.loadFormatted}, @${data.cpu.freqGhz}', 106 | ), 107 | SizedBox(height: AppMeasurements.serverStatGap.h), 108 | serverStats( 109 | FontAwesomeIcons.memory, 110 | '${data.ramUsed}/${data.ramSize} (${data.ram.usagePercentage})', 111 | ), 112 | SizedBox(height: AppMeasurements.serverStatGap.h), 113 | serverStats( 114 | Icons.sd_card_outlined, 115 | '${data.totalDiskUsed}/${data.totalDiskSize} (${data.diskUsagePercentageString})', 116 | ), 117 | SizedBox(height: AppMeasurements.serverStatGap.h), 118 | serverStats( 119 | Icons.lan_outlined, 120 | 'Down:${data.network.primaryDownload}, Up:${data.network.primaryUpload}', 121 | ), 122 | SizedBox(height: AppMeasurements.serverStatGap.h), 123 | Row( 124 | mainAxisAlignment: MainAxisAlignment.center, 125 | children: [ 126 | serverStats(Icons.thermostat, data.getTemperature), 127 | serverStats(Icons.sports_tennis, data.network.primaryPing), 128 | serverStats(Icons.alarm, data.getUptime), 129 | ], 130 | ), 131 | ], 132 | ); 133 | } 134 | 135 | Widget serverDetails(BasicDetails? data) { 136 | if (data == null) { 137 | return commonCircularProgressIndicator; 138 | } 139 | 140 | return Column( 141 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 142 | children: [ 143 | _buildServerHeader(data), 144 | _buildServerStats(data), 145 | ], 146 | ); 147 | } 148 | 149 | Stream fetchBasicDetailsLive(AppService appService) { 150 | final controller = StreamController(); 151 | Timer? timer; 152 | bool isFetching = false; 153 | 154 | void fetch() async { 155 | if (controller.isClosed || isFetching || !appService.connectionState) { 156 | return; 157 | } 158 | 159 | isFetching = true; 160 | try { 161 | final BasicDetails? basicDetails = 162 | await appService.commandExecuter.basicDetails; 163 | if (basicDetails != null && !controller.isClosed) { 164 | controller.add(basicDetails); 165 | } else if (!controller.isClosed) { 166 | // Handle null response - could log or emit error, but for now skip 167 | // Optionally: controller.addError('Failed to fetch basic details'); 168 | } 169 | } catch (e) { 170 | // Log the error instead of silently catching 171 | // Assuming a logging mechanism exists, e.g., logger.e('Error fetching basic details', e); 172 | if (!controller.isClosed) { 173 | controller.addError(e); 174 | } 175 | } finally { 176 | isFetching = false; 177 | if (!controller.isClosed) { 178 | timer = Timer(const Duration(seconds: 5), fetch); 179 | } 180 | } 181 | } 182 | 183 | controller.onListen = fetch; 184 | controller.onCancel = () { 185 | timer?.cancel(); 186 | controller.close(); 187 | }; 188 | 189 | return controller.stream; 190 | } 191 | -------------------------------------------------------------------------------- /lib/constants/constants.dart: -------------------------------------------------------------------------------- 1 | // Flutter imports: 2 | import 'package:flutter/material.dart'; 3 | 4 | class AppFontSizes { 5 | static const serverStatFontSize = 13.0; 6 | static const homePageIconFontSize = 60.0; 7 | static const appBarIconSize = 28.0; 8 | static const smallTileIconSize = 30.0; 9 | static const titleBarFontSize = 15.0; 10 | static const noDataFontSize = 10.0; 11 | static const systemToolsIcon = 25.0; 12 | static const systemToolsTittleFontSize = 15.0; 13 | static const systemToolsSubtitleFontSize = 8.0; 14 | static const fileSortIconSize = 40; 15 | static const settingsSaveIconSize = 50; 16 | static const appShortNameFontSize = 20.0; 17 | static const appLongNameFontSize = 15.0; 18 | static const appInfoLinkFontSize = 12.0; 19 | static const connectingFontSize = 15.0; 20 | static const notConnectedIconSize = 90.0; 21 | static const notConnectedFontSize = 15.0; 22 | static const dialogBoxactionFontSixe = 13.0; 23 | static const dialogBoxTitleFontSize = 15.0; 24 | static const breadCrumbFontSize = 13.0; 25 | static const customFolderNameSize = 20.0; 26 | static const noFilesFontSize = 30.0; 27 | static const fileSearchFontSize = 13.0; 28 | static const fileListTitleFontSize = 12.0; 29 | static const fileListSubtitleFontSize = 8.0; 30 | static const fileMenuIconSize = 20.0; 31 | static const dailogBoxTextFontSize = 12.0; 32 | } 33 | 34 | class AppMeasurements { 35 | static const appBarElevation = 1.0; 36 | static const appInfoIconHeight = 100.0; 37 | static const appInfoIconWidth = 100.0; 38 | static const deleteFileDailogBoxHeight = 50.0; 39 | static const kindleFormHeight = 200.0; 40 | static const serverStatGap = 2.0; 41 | static const diskUsageIndicatorSize = 300.0; 42 | static const diskUsageIndicatorStrokeWidth = 15.0; 43 | static const realTimeDetailsTopPosition = 15.0; 44 | static const realTimeDetailsHorizontalMargin = 10.0; 45 | } 46 | 47 | class AppDurations { 48 | static const diskUsageAnimation = Duration(milliseconds: 2000); 49 | } 50 | 51 | class AppConstants { 52 | static const appIconImageLocation = "assets/svgs/msm.svg"; 53 | static const appShortName = "MSM"; 54 | static const appFullName = "Media Server Manager"; 55 | static const appIssueFeatureReport = "Click Here To Submit Bugs"; 56 | static const issueReportUrl = "https://github.com/prinzpiuz/MSM/issues"; 57 | static const homePage = "Home Page"; 58 | static const homePageUrl = "https://prinzpiuz.in/MSM/"; 59 | static const license = "License"; 60 | static const licenseUrl = 61 | "https://github.com/prinzpiuz/MSM/blob/refactored/LICENSE"; 62 | static const alphaAndSpecialChars = r'/^[ A-Za-z_@./#&+-]*$/.'; 63 | static const emailvalidationRegex = 64 | r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?)*$"; 65 | static const upperLower = "[a-zA-Z]"; 66 | static const lowerCase = "[a-z]"; 67 | static const ipFormat = "[0-9.]"; 68 | static const numberOnly = "[0-9]"; 69 | static const macFormat = "[A-Z0-9:]"; 70 | static const connecting = "Connecting...."; 71 | static const notConnected = "Not Connected"; 72 | static const notAvailable = "Not Available"; 73 | static const connected = "Connected"; 74 | static const disconnected = "Disconnected"; 75 | static const uploadSize = 1073741824; 76 | static const deleteFilesTitle = "Delete These Files?"; 77 | static const renameFile = "Enter New Name"; 78 | static const moveFile = "Select The Folder To Move"; 79 | static const githubUpdateCommandsUrl = 80 | "https://raw.githubusercontent.com/prinzpiuz/MSM/refactored/linux_update_commands.json"; 81 | } 82 | 83 | class AppMessages { 84 | static const serverDetailSaved = "Saved Server Details"; 85 | static const folderConfigurationSaved = "Saved Folder Configurations"; 86 | static const serverFunctionSaved = "Saved Server Functions"; 87 | static const addMacAddress = "Make Sure You Save \n MAC Address Also"; 88 | static const selectFiles = "Select Files"; 89 | static const connectionLost = "Connection Lost"; 90 | static const uploadStarted = "Upload Will Start In Background"; 91 | static const errorOccurred = "Error While Uploading"; 92 | static const clearingTasks = "All Background Tasks Cleared"; 93 | static const folderCreationError = "Error Occurred While Creating Folders"; 94 | static const serverNotAvailable = "Server Not Available"; 95 | static const filesNotSelected = "Files Not Selected"; 96 | static const filesDeletedSuccessfully = "Files Deleted Successfully"; 97 | static const fileRename = "File Renamed Successfully"; 98 | static const moveFile = "File Moved Successfully"; 99 | static const sendToKindle = "File Successfully Sent To Kindle"; 100 | static const sendToKindleError = "File Sending To Kindle Failed \n Try Again"; 101 | static const setupKindleDetails = "Please Add Required Kindle Details"; 102 | static const sshKeyNotUploaded = "SSH Key Not Uploaded"; 103 | static const sshKeyUploaded = "SSH Key Uploaded Successfully"; 104 | static const fillDetails = "Fill Server Details First"; 105 | static const errorSavingFolderConfig = 106 | "An error occurred while saving folder configuration:"; 107 | static const folderVerifyError = 108 | "Unable to verify folder existence. Please try again."; 109 | } 110 | 111 | class BackgroundTaskUniqueNames { 112 | static const upload = "upload"; 113 | static const update = "update"; 114 | static const cleanServer = "cleanServer"; 115 | static const download = "download"; 116 | } 117 | 118 | class AppDictKeys { 119 | static const directory = "directory"; 120 | static const filePath = "filePath"; 121 | static const fileSize = "fileSize"; 122 | } 123 | 124 | class Identifiers { 125 | static const username = "username"; 126 | static const uptime = "uptime"; 127 | static const temperature = "temperature"; 128 | static const disk = "disk"; 129 | static const ram = "ram"; 130 | static const distribution = "distribution"; 131 | 132 | Identifiers._(); 133 | } 134 | 135 | class ContextKeys { 136 | static GlobalKey fileListingPageKey = 137 | GlobalKey(); 138 | static GlobalKey serverFunctionsPagekey = 139 | GlobalKey(); 140 | } 141 | 142 | class BackGroundTaskRelated { 143 | static const notificationChannelId = "background_notification"; 144 | static const foregroundServiceNotificationId = 888; 145 | static const initialNotificationContent = "Background Service Initializing"; 146 | static const initialNotificationTitle = "MSM"; 147 | static const runningBody = "Background Service Running"; 148 | static const icon = "ic_bg_service_small"; 149 | static const stopActionId = "stop_service"; 150 | static const stopActionTitle = "Stop Service"; 151 | static const uploadChannelId = "upload"; 152 | static const uploadChannelName = "upload channel"; 153 | static const uploadNotificationId = 123; 154 | } 155 | 156 | class ShellScriptPaths { 157 | static const basicDetails = "shell_scripts/basic_details.sh"; 158 | } 159 | --------------------------------------------------------------------------------