diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 8cbcbb2..1cb7dda 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -7,7 +7,7 @@ on: paths: - "pubspec.yaml" pull_request: - branches: ["main"] + branches: [ main ] paths: - '.github/**' workflow_dispatch: diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..056ca21 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "netr", + "request": "launch", + "type": "dart" + }, + { + "name": "netr (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "netr (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bb8ef4e..1376cb3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..b2e67a9 --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/docs/flow.puml b/docs/flow.puml new file mode 100644 index 0000000..464d12b --- /dev/null +++ b/docs/flow.puml @@ -0,0 +1,208 @@ +@startuml +autonumber + +actor user +boundary "Main App" as mainApp +boundary "Splash Screen" as splash +boundary "Update Screen" as update +boundary "App Home" as home +boundary "Live View Page" as live +boundary "Snack Bar" as snack +participant "PlayerBase" as pb +control "AppInitializationCubit" as aic +control "AppUpdateCubit" as auc +control "ThemeCubit" as tc +control "NavigationCubit" as nc +control "LocationCubit" as lc +control "RunConfigCubit" as rcc +control "LiveViewCubit" as lvc +control "ViewerKeyboardCubit" as vkbc +control "VideoPlayerCubit" as vpc +control "LiveCameraViewCubit" as lcvc +entity "Flutter" as flutter + +user -> mainApp: Start App +mainApp -> aic : Triggers on creation +aic -> mainApp: emit state = initialization +'ais_state --up--> mainApp: 2.2 Triggers BlocBuilder +mainApp -> splash: Build Splash Screen +aic -> flutter: Is mobile platform? +alt "Mobile Platform" + autonumber 1 + aic -> flutter: Get version details + alt "Update is required" + autonumber 1 + aic -> mainApp: emit state = updateApp + mainApp -> update: Build Update Screen + update -> auc: Create cubit + auc -> update: emit state = userInput + update -> user: Ask for user confirmation + alt "User allows update" + autonumber 1 + update -> auc: Try OTA update + auc -> update: emit state = inProgress + update -> user: Show Progress + alt "Success" + auc -> user: User presented with new version of App + else "Failure" + auc -> update: emit state = error + update -> snack: Show error + update -> mainApp: emit state = initialized + end + else + update -> mainApp: emit state = initialized + end + else "Failure to check if update is required" + autonumber 1 + aic -> mainApp: emit state = updateCheckFailed + mainApp -> snack: Show snack + end +else + autonumber 6 + aic -> mainApp: emit state = intialized +end + +autonumber 6 +aic -> mainApp: emit state = initialized + +autonumber 1 +mainApp -> home: Build Home Screen +home -> tc: setInitialTheme() +tc -> flutter: Read if black theme is to be used +tc -> home: Build Home with Theme +home -> nc: Create Navigation Cubit and set page as 0 +home -> lc: Create Location Cubit and call determinePosition() +lc -> flutter: Check if location service is enabled +alt "Service is enabled" + lc -> flutter: Check Permission + alt "If LocationPermission.denied" + lc -> flutter: Request Permission + flutter -> user: Present user with permission grant dialog + alt "User doesn't allow permission" + lc -> home: emit state = LocationState.permissionsDenied + ... Code returns from here ... + end + else "If LocationPermission.deniedForever" + lc -> home: emit state = LocationPermission.deniedForever + ... Code returns from here ... + end + + lc -> flutter: Get current position + alt "If Geocoder is present" + lc -> flutter: Resolve using geocoder + lc -> home: emit state = LocationState.locatedFully + else + lc -> home: emit state = LocationState.locatedWithCordinates + end + ... Code returns from here ... +else "Service is not enabled" + lc -> home: emit state = locationServiceDisabled + ... Code returns from here ... +else "Failure" + lc -> home: emit state = permissionDeniedForever + ... Code returns from here ... +end +home -> rcc: Create RunConfigCubit +rcc -> flutter: load setting location items +rcc -> flutter: load setting run.archiveDate +rcc -> home: emit state +home -> live: Show live view page +live -> lvc: Create Live View Cubit with LiveViewInitialState +lvc -> flutter: Load settings +lvc -> live: emit state LiveViewUpdatedState +alt "state is LiveViewInitialState" + live -> user: Present empty view to user +else "state is LiveViewUpdatedState" + alt "If camera and location is null" + live -> user: Present list of cameras by location + ... Wait for user input ... + user -> live: User selects a camera and location + live -> lvc: updateSelectedCameraAndLocation() + lvc -> live: emit state = LiveViewUpdatedState(camera, location) + else "If state has camera and location not null" + live -> pb: Create object with state values + pb -> flutter: Create TransformationController() + pb -> flutter: Create Player() + pb -> flutter: Create VideoController() + pb -> flutter: start hide timer + pb -> flutter: set isInitialized = true and setState() + pb -> vkbc: Create cubit with Transformation Controller and state = ViewerKeyboardInitialState + alt "If isInitialized == false" + live -> user: Show Circular Progress Indicator + else "If isInitialized == true" + live -> vpc: Create VideoPlayerCubit with state VideoPlayerUpdateState() + live -> lcvc: Create LiveCameraViewCubit with state LiveCameraViewState() and store stream, camera, location, credential, quality.high, archive + alt "If playerStream.buffering.listen == buffering" + vkbc -> live: emit state = LiveCameraViewBufferingState(50.0, false) + live -> snack: Show buffering % + else "If buffering done" + vkbc -> live: emit state = LiveCameraViewBufferingState(100.0, true) + live -> snack: Show buffering done + else "If error" + vkbc -> live: emit state = LiveCameraViewErrorState(error) + end + alt "If playerStream.playing.listen == playing" + vkbc -> live: emit state = LiveCameraViewPlayingState(playing) + alt "If state.playing = true" + live -> snack: Started playing + else + live -> snack: Stopped playing + end + else "onError or playerStream.error.listen error" + vkbc -> live: emit state = LiveCameraViewErrorState(error)' + live -> snack: Show error + live -> lvc: call back() + else "onDone" + vkbc -> live: emit state = LiveCameraViewDoneState + end + alt "playerStream.width.listen((width) or playerStream.height.listen((height)" + vkbc -> live: emit state = emit state = LiveCameraViewVideoState(width, height) + alt "If state.width > 0 and state.height > 0" + live -> vpc: updateWidthHeight(state.width, state.height) + vpc -> live: emit state VideoPlayerState(width, height, aspectratio) + alt "If state.width > 0 and state.height > 0" + live -> flutter: Create Video object + else + live -> lcvc: call getStreamUrl() + lcvc -> live: LiveCameraViewUpdatedState(url) + live -> user: Show CircularProgressIndicator + end + end + end + alt LiveCameraViewUpdatedState + live -> flutter: Player.open(state.url) + live -> user: Show video playing for camera and location + ... Video play back continues untill any error or user action ... + end + user -> vkbc: User presses any keyboard key or Taps screen + alt "Select/Enter/ContextMenu or Screen Tap or left/right button click" + vkbc -> pb: emit state = ViewerKeyboardTapState + pb -> flutter: Set _controlsVisible = true and setState() + flutter -> live: Show controls + live -> flutter: Start hide timer + else "Space/MediaPlay/MediaPlayPause/MediaPause" + vkbc -> pb: emit state = ViewerKeyboadPlayState(PlayActions.playToggle) + pb -> flutter: toggle play on Player + else "Not zoomed in and (Left Arrow Key or drag left with mouse or finger)" + vkbc -> pb: emit state = ViewerKeyboadPlayState(PlayActions.previous) + pb -> lvc: call previous() + lvc -> pb: emit state = LiveViewUpdatedState with previous camera + else "Not zoomed in and (Right Arrow Key or drag right with mouse or finger)" + vkbc -> pb: emit state = ViewerKeyboadPlayState(PlayActions.next) + pb -> lvc: call next() + lvc -> pb: emit state = LiveViewUpdatedState with next camera + else "Not zoomed in and Backspace/Escape Key" + vkbc -> pb: emit state = ViewerKeyboardBackState + pb -> lvc: call back() + lvc -> pb: emit state = LiveViewUpdatedState with camera = null, location = null + ... Will lead to list of cameras ... + else "If Zoomed in and (\nZoomIn/Add/MediaFastFwd or pinch to zoom or scroll wheel up in or \nZoomOut/Minus/MediaRewind or pinch to zoom out or scroll wheel down or \nLeft/Right/Up/Down Arrow or Drag with mouse or finger or \nBackspace/Escape or Tap Back)" + vkbc -> pb: emit ViewerKeyboardControllerState(controllerValue) using _scaleAndCenter or _translate + pb -> flutter: Set controller value (Handles translation and zoom) + end + end + end +end + + +@enduml \ No newline at end of file diff --git a/docs/flow.svg b/docs/flow.svg new file mode 100644 index 0000000..4e58309 --- /dev/null +++ b/docs/flow.svg @@ -0,0 +1 @@ +useruseruseruseruseruseruseruseruserMain AppMain AppMain AppMain AppMain AppMain AppMain AppMain AppMain AppSplash ScreenSplash ScreenSplash ScreenSplash ScreenSplash ScreenSplash ScreenSplash ScreenSplash ScreenSplash ScreenUpdate ScreenUpdate ScreenUpdate ScreenUpdate ScreenUpdate ScreenUpdate ScreenUpdate ScreenUpdate ScreenUpdate ScreenApp HomeApp HomeApp HomeApp HomeApp HomeApp HomeApp HomeApp HomeApp HomeLive View PageLive View PageLive View PageLive View PageLive View PageLive View PageLive View PageLive View PageLive View PageSnack BarSnack BarSnack BarSnack BarSnack BarSnack BarSnack BarSnack BarSnack BarPlayerBasePlayerBasePlayerBasePlayerBasePlayerBasePlayerBasePlayerBasePlayerBasePlayerBaseAppInitializationCubitAppInitializationCubitAppInitializationCubitAppInitializationCubitAppInitializationCubitAppInitializationCubitAppInitializationCubitAppInitializationCubitAppInitializationCubitAppUpdateCubitAppUpdateCubitAppUpdateCubitAppUpdateCubitAppUpdateCubitAppUpdateCubitAppUpdateCubitAppUpdateCubitAppUpdateCubitThemeCubitThemeCubitThemeCubitThemeCubitThemeCubitThemeCubitThemeCubitThemeCubitThemeCubitNavigationCubitNavigationCubitNavigationCubitNavigationCubitNavigationCubitNavigationCubitNavigationCubitNavigationCubitNavigationCubitLocationCubitLocationCubitLocationCubitLocationCubitLocationCubitLocationCubitLocationCubitLocationCubitLocationCubitRunConfigCubitRunConfigCubitRunConfigCubitRunConfigCubitRunConfigCubitRunConfigCubitRunConfigCubitRunConfigCubitRunConfigCubitLiveViewCubitLiveViewCubitLiveViewCubitLiveViewCubitLiveViewCubitLiveViewCubitLiveViewCubitLiveViewCubitLiveViewCubitViewerKeyboardCubitViewerKeyboardCubitViewerKeyboardCubitViewerKeyboardCubitViewerKeyboardCubitViewerKeyboardCubitViewerKeyboardCubitViewerKeyboardCubitViewerKeyboardCubitVideoPlayerCubitVideoPlayerCubitVideoPlayerCubitVideoPlayerCubitVideoPlayerCubitVideoPlayerCubitVideoPlayerCubitVideoPlayerCubitVideoPlayerCubitLiveCameraViewCubitLiveCameraViewCubitLiveCameraViewCubitLiveCameraViewCubitLiveCameraViewCubitLiveCameraViewCubitLiveCameraViewCubitLiveCameraViewCubitLiveCameraViewCubitFlutterFlutterFlutterFlutterFlutterFlutterFlutterFlutterFlutteruseruserMain AppMain AppSplash ScreenSplash ScreenUpdate ScreenUpdate ScreenApp HomeApp HomeLive View PageLive View PageSnack BarSnack BarPlayerBasePlayerBaseAppInitializationCubitAppInitializationCubitAppUpdateCubitAppUpdateCubitThemeCubitThemeCubitNavigationCubitNavigationCubitLocationCubitLocationCubitRunConfigCubitRunConfigCubitLiveViewCubitLiveViewCubitViewerKeyboardCubitViewerKeyboardCubitVideoPlayerCubitVideoPlayerCubitLiveCameraViewCubitLiveCameraViewCubitFlutterFlutter1Start App2Triggers on creation3emit state = initialization4Build Splash Screen5Is mobile platform?alt["Mobile Platform"]1Get version detailsalt["Update is required"]1emit state = updateApp2Build Update Screen3Create cubit4emit state = userInput5Ask for user confirmationalt["User allows update"]1Try OTA update2emit state = inProgress3Show Progressalt["Success"]4User presented with new version of App["Failure"]5emit state = error6Show error7emit state = initialized8emit state = initialized["Failure to check if update is required"]1emit state = updateCheckFailed2Show snack6emit state = intialized6emit state = initialized1Build Home Screen2setInitialTheme()3Read if black theme is to be used4Build Home with Theme5Create Navigation Cubit and set page as 06Create Location Cubit and call determinePosition()7Check if location service is enabledalt["Service is enabled"]8Check Permissionalt["If LocationPermission.denied"]9Request Permission10Present user with permission grant dialogalt["User doesn't allow permission"]11emit state = LocationState.permissionsDeniedCode returns from here["If LocationPermission.deniedForever"]12emit state = LocationPermission.deniedForeverCode returns from here13Get current positionalt["If Geocoder is present"]14Resolve using geocoder15emit state = LocationState.locatedFully16emit state = LocationState.locatedWithCordinatesCode returns from here["Service is not enabled"]17emit state = locationServiceDisabledCode returns from here["Failure"]18emit state = permissionDeniedForeverCode returns from here19Create RunConfigCubit20load setting location items21load setting run.archiveDate22emit state23Show live view page24Create Live View Cubit with LiveViewInitialState25Load settings26emit state LiveViewUpdatedStatealt["state is LiveViewInitialState"]27Present empty view to user["state is LiveViewUpdatedState"]alt["If camera and location is null"]28Present list of cameras by locationWait for user input29User selects a camera and location30updateSelectedCameraAndLocation()31emit state = LiveViewUpdatedState(camera, location)["If state has camera and location not null"]32Create object with state values33Create TransformationController()34Create Player()35Create VideoController()36start hide timer37set isInitialized = true and setState()38Create cubit with Transformation Controller and state = ViewerKeyboardInitialStatealt["If isInitialized == false"]39Show Circular Progress Indicator["If isInitialized == true"]40Create VideoPlayerCubit with state VideoPlayerUpdateState()41Create LiveCameraViewCubit with state LiveCameraViewState() and store stream, camera, location, credential, quality.high, archivealt["If playerStream.buffering.listen == buffering"]42emit state = LiveCameraViewBufferingState(50.0, false)43Show buffering %["If buffering done"]44emit state = LiveCameraViewBufferingState(100.0, true)45Show buffering done["If error"]46emit state = LiveCameraViewErrorState(error)alt["If playerStream.playing.listen == playing"]47emit state = LiveCameraViewPlayingState(playing)alt["If state.playing = true"]48Started playing49Stopped playing["onError or playerStream.error.listen error"]50emit state = LiveCameraViewErrorState(error)'51Show error52call back()["onDone"]53emit state = LiveCameraViewDoneStatealt["playerStream.width.listen((width) or playerStream.height.listen((height)"]54emit state = emit state = LiveCameraViewVideoState(width, height)alt["If state.width > 0 and state.height > 0"]55updateWidthHeight(state.width, state.height)56emit state VideoPlayerState(width, height, aspectratio)alt["If state.width > 0 and state.height > 0"]57Create Video object58call getStreamUrl()59LiveCameraViewUpdatedState(url)60Show CircularProgressIndicatoralt[LiveCameraViewUpdatedState]61Player.open(state.url)62Show video playing for camera and locationVideo play back continues untill any error or user action63User presses any keyboard key or Taps screenalt["Select/Enter/ContextMenu or Screen Tap or left/right button click"]64emit state = ViewerKeyboardTapState65Set _controlsVisible = true and setState()66Show controls67Start hide timer["Space/MediaPlay/MediaPlayPause/MediaPause"]68emit state = ViewerKeyboadPlayState(PlayActions.playToggle)69toggle play on Player["Not zoomed in and (Left Arrow Key or drag left with mouse or finger)"]70emit state = ViewerKeyboadPlayState(PlayActions.previous)71call previous()72emit state = LiveViewUpdatedState with previous camera["Not zoomed in and (Right Arrow Key or drag right with mouse or finger)"]73emit state = ViewerKeyboadPlayState(PlayActions.next)74call next()75emit state = LiveViewUpdatedState with next camera["Not zoomed in and Backspace/Escape Key"]76emit state = ViewerKeyboardBackState77call back()78emit state = LiveViewUpdatedState with camera = null, location = nullWill lead to list of cameras["If Zoomed in and (ZoomIn/Add/MediaFastFwd or pinch to zoom or scroll wheel up in orZoomOut/Minus/MediaRewind or pinch to zoom out or scroll wheel down orLeft/Right/Up/Down Arrow or Drag with mouse or finger orBackspace/Escape or Tap Back)"]79emit ViewerKeyboardControllerState(controllerValue) using _scaleAndCenter or _translate80Set controller value (Handles translation and zoom) \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index e406903..9fdaa6b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2.9.3+1105 +version: 2.9.4+1106 homepage: https://github.com/homebackend/netr/ repository: https://github.com/homebackend/netr/ diff --git a/release-key-base64.txt b/release-key-base64.txt new file mode 100644 index 0000000..803fb80 --- /dev/null +++ b/release-key-base64.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/release-key.jks b/release-key.jks new file mode 100644 index 0000000..ee3bb45 Binary files /dev/null and b/release-key.jks differ