Feature/multiple mirroring cutouts - #56
Merged
Merged
Conversation
- add ScreenCutout serializable data model to represent crop/placement pairs - update MacroPadLayout to store list of mirror cutouts in layout configurations - implement coordinate projections, boundary drag clamping, and collision-safe resize logic - add unit tests for coordinate calculations and cutout boundary manipulations - implement profile loading viewport migrations and active layout surface size migrations - add MirrorCutoutDomainTest verifying layout migration and TouchProjectionController mapping - rewrite ScreenCaptureService to spin up and manage multiple virtual displays - rewrite MirrorPresentation to render cutouts inside clipping parent container frames - rewrite DirectMirrorServer and DirectMirrorSurfaceBridge to support multi-surface binder transactions - create CropSelectorOverlay for choosing source crops on the primary display - create CutoutLayoutEditor to arrange and resize cutout placements on the secondary display - add English and German string resources for the layout and crop editors - update mirror and privileged-mode FEATURE.md feature specifications - compile and package update for megingiard_mirror.dex
- Migrate cutout views in MirrorPresentation from SurfaceView to TextureView - Apply crop transforms directly via TextureView.setTransform(Matrix) - Restrict MediaProjection mirroring to a single cutout to avoid GPU resource freeze - Introduce CropSelectorActivity to launch the crop overlay on the primary display - Use rememberUpdatedState in CutoutLayoutEditor and CropSelectorOverlay gesture detectors - Convert HANDLE_SIZE to pixels using density to correctly center resize handles - Gate adding cutouts on privileged mode in the editor toolbar and show warnings - Update technical documentation in ARCHITECTURE.md and FEATURE.md
…urface - implement MultiCutoutContainer FrameLayout to render a single master TextureView multiple times via canvas transformations - refactor ScreenCaptureService to manage a single VirtualDisplay and master surface instead of maps of displays and surfaces - update startPrivdPath and updateDirectServerSurfaces to send only the master surface to the privileged daemon - remove non-privileged multi-cutout editor restrictions as MediaProjection mode now safe to mirror multiple cutouts - update mirror feature specifications in FEATURE.md to document the single-surface duplication architecture
- remove edge padding from the cutout layout editor Box container in MirrorPresentation.kt - define MP_EDGE_ZONE in CutoutLayoutEditor.kt and apply it dynamically to the floating toolbar card - reposition corner resize handles inside the bounding boxes in CropSelectorOverlay.kt to prevent screen-edge clipping - refactor crop box dragging and resize gestures in CropSelectorOverlay.kt to accumulate delta offsets for jitter-free 1-to-1 tracking
- align CutoutLayoutEditor coordinate space with presentation surface bounds - split single-viewport and multi-cutout drawing paths in MultiCutoutContainer.dispatchDraw to avoid double-scaling
- size MultiCutoutContainer FrameLayout to MATCH_PARENT to allow cutouts over the whole screen area - set ScreenCaptureManager surface size to the target window dimensions - implement aspect-ratio fitting and centering in MultiCutoutContainer.dispatchDraw for single-viewport mode
- use remember(cutout.id) to store dragStartX/Y/W/H in CutoutLayoutEditor.kt - use remember(cutoutId) to store dragStartX/Y/W/H in CropSelectorOverlay.kt - add missing Composable state imports (remember, mutableStateOf, setValue) in both files
…out with an empty slate - Enable deleting the last cutout from the layout editor floating toolbar. - Initialize multi-cutout mode with an empty slate if toggled from the default single full-screen layout. - Add `mirrorConfigured` flag to `PadLayout` to avoid overwriting intentionally empty cutout lists during startup migration. - Update `loadFrom`, `addLayout`, `updateLayout`, and coordinate/cutout saving in `MacroPadState` to set `mirrorConfigured = true`. - Synchronize mirror feature documentation (`docs/features/mirror/FEATURE.md`) with the new multi-cutout behaviors. - Add unit tests verifying load migration logic for configured vs unconfigured layouts in `MirrorCutoutDomainTest`.
…nd resize gestures - Replace collision snap-back logic in `clampCutoutDrag` and `clampCutoutResize` with real-time border clamping. - Allow cutouts to smoothly slide along collision boundaries during drag gestures. - Clamp resize bounds precisely against blocking cutouts instead of reverting the dimensions to gesture start states. - Update unit tests in `MirrorCoordinateTransformTest` to assert clamped move and resize dimensions rather than full reverts.
…drift - use unconstrained target coordinates for cross-axis overlap checks in clampCutoutResize - eliminate order-dependence between X and Y clamping that caused vertical scaling to fail clamp checks - add a unit test asserting correct vertical clamping with slight horizontal gesture drift
…sions - calculate distX and distY clamp distances when corner resizing collides on both axes - resolve the collision by clamping only the axis with the smaller clamp distance - allow cutouts to touch on one border while sliding/scaling freely along the other - add unit tests covering vertical-horizontal and horizontal-vertical cross-boundary resizes
…nt corners - compute distX and distY using original starting coordinates instead of targets - ensure stable axis selection for clamping throughout a continuous drag gesture - add a test case asserting stable horizontal/vertical clamping around adjacent corners
- Retrieve the previous frame's clamped coordinates (`prevX`, `prevY`) from the layout state in `clampCutoutResize` - Use the previous frame's coordinates instead of static starting coordinates (`originalX`, `originalY`) to compute distances for deciding which axis to clamp on collision - Add an adjacent-starting-position test case to `MirrorCoordinateTransformTest` to verify that dragging past corners clamps smoothly without snapping back on the perpendicular axis
…ple cutouts - Implement topological side-entry checks (`prevXOverlaps` and `prevYOverlaps`) inside `clampCutoutResize` to determine the correct clamping axis based on the cutout's relation to obstacles in the previous frame - Update target coordinates sequentially within the collision loop so that subsequent obstacle overlap checks evaluate the already-clamped coordinates - Add a multi-obstacle unit test to `MirrorCoordinateTransformTest` to verify correct clamping against multiple neighboring borders
…tely - Add `mirrorMultiMode` flag to `PadLayout` schema to track active viewport mode - Decouple `saveMirrorViewport` so that single-viewport scale/offsets do not overwrite `mirrorCutouts` - Generate single full-screen cutout dynamically at runtime in `ScreenCaptureManager` when `mirrorMultiMode` is false - Update `CutoutLayoutEditor` toggles to switch modes by setting `mirrorMultiMode` without clearing configurations - Remove legacy viewport migrations and update unit tests in `MirrorCutoutDomainTest` to match the dual-mode persistence model
…ze clamping - Log at start of move/resize gestures with starting coordinates - Log target vs clamped coordinates during dragging and resizing only when clamping changes the values to prevent event spam
…ragging - Evaluate single-axis slide configurations (Candidate 1: clamp X only, Candidate 2: clamp Y only) when a cutout overlaps another. - Select the candidate that minimizes distance to the user's target pointer coordinate to prevent abrupt corner snapping/jumping. - Add regression test case in MirrorCoordinateTransformTest.kt verifying closer-candidate selection during diagonal drag.
- Pass curCutout.destX and curCutout.destY as reference coordinates to clampCutoutDrag in CutoutLayoutEditor instead of dragStartX and dragStartY. - This ensures that active clamping checks track the cutout's current position rather than its static drag-start position, preventing incorrect snap-backs when dragging around or past obstacles. - Add regression test case in MirrorCoordinateTransformTest.kt verifying correct clamping relative to previous frame coordinates.
- add keepAspectRatio flag to ScreenCutout schema - implement aspect ratio fitting and dominant-axis resizing in MirrorCoordinateTransform - add binary search collision resolution engine to preserve aspect ratios on collision - add Aspect Lock toggle button to CutoutLayoutEditor and localize strings - add unit tests verifying aspect-ratio-locked resizing and collision behavior - document FR-M12 and binary search clamping in FEATURE.md
…itors - add drag handles to top and bottom screen mirror edit toolbars - convert bottom layout editor toolbar buttons into compact icons - redesign primary screen crop card into a horizontal toolbar with X (Cancel) and checkmark (Done) buttons - implement crop cancellation to revert cutout crop changes to original dimensions - add localized string resources for cancel actions and drag descriptions
- replace drag handle icon with DragIndicator (6 dots) in both editors - increase hit target size of grab handles to 36dp x 32dp for easier dragging - add 1:1 / 1:X text indicator next to the Aspect Ratio Lock button in CutoutLayoutEditor
- hide status bar and gesture navigation indicator in CropSelectorActivity - set transient swipe behavior so system bars auto-hide after swipes
- add label parameter to ToolbarIconButton in CutoutLayoutEditor - move the 1:1 / 1:X text indicator inside the Aspect Ratio Lock button - remove the separate aspect ratio status text Box
…ing crop resizing - Capture gesture-start destination width and height in `onDragStart` of all four resize handles inside `CropSelectorOverlay.kt` - Pass the starting dimensions to `updateCutoutWithNewCrop` on every `onDrag` event - Update `updateCutoutWithNewCrop`'s scaling logic to calculate destination sizes that fit within both the starting bounds and screen margins while strictly maintaining the new crop aspect ratio
- Decrease BORDER_WIDTH from 2.dp to 1.dp in `CropSelectorOverlay` and `CutoutLayoutEditor` - Apply 50% opacity to active crop and cutout borders (using copy(alpha = 0.5f)) - Apply 15% opacity to inactive layout borders (using copy(alpha = 0.15f)) - Apply 50% background transparency to corner resize handles in `ResizeHandleView`
…utouts - Freeze screen capture stream in `CropSelectorActivity.onCreate` before the crop overlay is drawn - This ensures the captured frame is a clean display capture (e.g. of the game) - Restore initial freeze/live state on activity destruction if the activity is finishing - Save and restore initial freeze state across configuration changes (activity recreations) using instance state bundle
- Define `mirror_crossfade_touching_borders` settings key in `SettingsKeys` and `MirrorSettings` - Add English and German localization resources for the new crossfade option - Expose the crossfade setting in `GlobalSettingsViewModel` and add a toggle row in `GlobalSettingsScreen` General section - Implement real-time adjacent cutout edge detection in `MirrorPresentation.dispatchDraw` - Extend clipping/destination bounds of touching cutouts to overlap by half of the blend width (16dp) - Apply linear gradient alpha masks with `DST_IN` xfermode on the overlapping boundaries to create a smooth crossfade blend
- Replace global crossfade checkbox row in GlobalSettingsScreen and GlobalSettingsViewModel with a toolbar slider. - Add dynamic blend width slider to the floating toolbar in CutoutLayoutEditor, visible only in multi-cutout edit mode. - Bind the slider value dynamically from 0 to 100 dp, backing it with the persistent `KEY_CROSSFADE_BLEND_WIDTH_DP` float setting in SettingsKeys and MirrorSettings. - Update MultiCutoutContainer in MirrorPresentation to compute pixel-based gradient widths dynamically and invalidate layout drawing when the slider value changes. - Replace old crossfade settings strings with new slider labels in English and German localized resources.
- Symmetrically apply linear gradient fades to both touching cutouts. - Set layer paint xfermode to PorterDuff.Mode.ADD during compositing, ensuring the combined opacity of overlapping regions equals 1.0 (100%) and preventing the black background from bleeding through.
- Extend linear gradient drawRect dimensions to cover the full extended cutout bounding box. - Rely on LinearGradient CLAMP mode to keep the non-overlapping canvas opaque, moving anti-aliased subpixel rendering edges to the outer boundaries of the layer.
…touts - Remove manual M3 SliderDefaults.colors overrides in BackgroundSettingsOverlay to comply with theme guidelines - Detach mirrorVirtualDisplay surface in ScreenCaptureService when presentation surface is destroyed - Replace Triple destructuring lambda in MirrorPresentation to resolve Kotlin compiler warning - Throttle EMA motion smoothing updates in MirrorPresentation to configured frame rate to avoid excessive GPU-to-CPU readback - Restrict single-cutout follow-touch centering in MirrorPresentation to uncropped viewports
…roring start - Create default cutout during ScreenCaptureService startup if the layout's cutout list is empty (for both privileged and non-privileged flows) - Handle layout switching in MirrorViewportController by dynamically initializing a default cutout if empty using the active surface dimensions - Add unit test verifying that restoreFromLayout auto-populates empty cutout lists with the correct default aspect ratio cutout based on screen sizes
- Extract inline padding literals to ASO_SPACING_8 constant in BackgroundSettingsOverlay - Capture TextureView frame once and reuse scratchBitmap to avoid multiple readbacks in MirrorPresentation - Send UP touch injection and reset gesture states on missing cutout in TouchProjectionController - Add hasLoadedOnce flag in MacroPadSettings to prevent initial load early-return bug - Fit default cutout destination bounds to limiting axis in ScreenCutout - Remove dead recordingVirtualDisplay property in ScreenCaptureService - Update createDefault unit test assertions in MirrorCoordinateTransformTest
Comment on lines
+103
to
+109
| override fun onDestroy() { | ||
| super.onDestroy() | ||
| AppLog.i(TAG, "onDestroy: isFinishing=$isFinishing") | ||
| if (isFinishing && !wasFrozenInitially) { | ||
| ScreenCaptureManager.setFrozen(false) | ||
| } | ||
| } |
Comment on lines
+456
to
+468
| Surface( | ||
| modifier = Modifier | ||
| .align(Alignment.BottomCenter) | ||
| .offset { toolbarOffset } | ||
| .padding(bottom = 32.dp) | ||
| .shadow(CARD_SHADOW, RoundedCornerShape(CARD_CORNER)), | ||
| color = colors.surface.copy(alpha = 0.9f), | ||
| shape = RoundedCornerShape(CARD_CORNER), | ||
| border = androidx.compose.foundation.BorderStroke(1.dp, colors.controlOverlayBorder) | ||
| ) { | ||
| Box( | ||
| modifier = Modifier.padding(top = 4.dp, bottom = 4.dp, start = 4.dp, end = 8.dp) | ||
| ) { |
Comment on lines
+390
to
+394
| // Motion Smoothing Slider Row | ||
| Row( | ||
| modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), | ||
| verticalAlignment = Alignment.CenterVertically | ||
| ) { |
Comment on lines
+84
to
+90
| Icon( | ||
| imageVector = Icons.Rounded.Edit, | ||
| contentDescription = stringResource(R.string.cd_viewport_edit), | ||
| tint = if (isCapturing) colors.accent else colors.onControlOverlay.copy(alpha = 0.3f), | ||
| modifier = Modifier.size(16.dp) | ||
| ) | ||
| Spacer(Modifier.width(6.dp)) |
…tate locking - app/src/main/java/com/stormpanda/megingiard/mirror/CropSelectorActivity.kt: reset AppStateManager.activeCropCutoutId to null when activity is finishing to prevent state locking - app/src/main/java/com/stormpanda/megingiard/macropad/BackgroundSettingsOverlay.kt: extract inline 4.dp row spacing to ASO_CUTOUT_ROW_V_PADDING file-scoped constant - app/src/main/java/com/stormpanda/megingiard/ui/PillMenu.kt: declare PM_SCREEN_MIRRORING_ICON_SIZE and PM_SCREEN_MIRRORING_SPACER_W constants - app/src/main/java/com/stormpanda/megingiard/ui/PillMirrorCard.kt: replace inline metrics in MirrorControlCard with PM_* constants - app/src/main/java/com/stormpanda/megingiard/mirror/CropSelectorOverlay.kt: extract remaining ToolbarIconButton inline metrics to CS_* constants
Comment on lines
+70
to
+72
|
|
||
|
|
||
|
|
| } | ||
|
|
||
|
|
||
|
|
Comment on lines
123
to
+127
| data.enforceInterface(DIRECT_SURFACE_DESCRIPTOR); | ||
| if (data.readInt() == 0) { | ||
| System.err.println("DirectMirrorServer: app sent no surface"); | ||
|
|
||
| // Release all existing resources first before applying the new configuration | ||
| release(); | ||
|
|
Comment on lines
15
to
+35
| @@ -25,8 +26,13 @@ internal object DirectMirrorSurfaceBridge { | |||
| val reply = Parcel.obtain() | |||
| return try { | |||
| data.writeInterfaceToken(DIRECT_SURFACE_DESCRIPTOR) | |||
| data.writeInt(1) | |||
| surface.writeToParcel(data, 0) | |||
| data.writeInt(validSurfaces.size) | |||
| for ((surface, width, height) in validSurfaces) { | |||
| data.writeInt(width) | |||
| data.writeInt(height) | |||
| data.writeInt(1) | |||
| surface.writeToParcel(data, 0) | |||
| } | |||
…ary screen - app/src/main/java/com/stormpanda/megingiard/mirror/CropSelectorOverlay.kt: import Dp and declare CS_TOUCH_AREA_RATIO constant - app/src/main/java/com/stormpanda/megingiard/mirror/CropSelectorOverlay.kt: update ResizeHandleView to support dynamic touchWidth/touchHeight and center visual handle - app/src/main/java/com/stormpanda/megingiard/mirror/CropSelectorOverlay.kt: calculate expanded target bounds and center the corner handle touch offsets accordingly
Comment on lines
+243
to
251
| targetFollowX = targetSrcX | ||
| targetFollowY = targetSrcY | ||
| ensureFollowAnimationRunning(targetCutout.id) | ||
| } | ||
| } | ||
|
|
||
| private fun ensureFollowAnimationRunning() { | ||
| private fun ensureFollowAnimationRunning(cutoutId: String) { | ||
| if (followAnimationJob?.isActive == true) return | ||
| followAnimationJob = scope.launch { |
Comment on lines
285
to
295
| fun setBackgroundSettingsActive(active: Boolean) { | ||
| AppLog.i(TAG, "setBackgroundSettingsActive($active)") | ||
| if (active) { | ||
| _isFullscreenKeyboardActive.value = false | ||
| _isFullscreenMouseActive.value = false | ||
| _isViewportEditActive.value = false | ||
| } else { | ||
| _isViewportEditActive.value = true | ||
| } | ||
| _isBackgroundSettingsActive.value = active | ||
| } |
Comment on lines
+1035
to
+1038
| override fun invalidate() { | ||
| val now = System.currentTimeMillis() | ||
| val interval = if (maxFps >= 60) 0L else (1000L / maxFps) | ||
| if (interval == 0L || now - lastInvalidateTime >= interval) { |
- domain/src/main/java/com/stormpanda/megingiard/mirror/ScreenCaptureManager.kt: track followAnimationCutoutId and restart follow animation job when active follow cutout changes - domain/src/main/java/com/stormpanda/megingiard/AppStateManager.kt: store and restore viewport edit active state when opening/closing background settings overlay - app/src/main/java/com/stormpanda/megingiard/mirror/MirrorPresentation.kt: clamp maxFps to at least 1 to prevent division-by-zero in invalidate() - app/src/main/java/com/stormpanda/megingiard/mirror/MirrorPresentation.kt: remove unused PixelCopy, Matrix, MirrorSettings, and abs imports - app/src/main/java/com/stormpanda/megingiard/macropad/LayoutTemplateOverlay.kt: remove unused ScreenCutout import - app/src/main/java/com/stormpanda/megingiard/macropad/BackgroundSettingsOverlay.kt: remove unused SliderDefaults import - domain/src/main/java/com/stormpanda/megingiard/settings/MirrorSettings.kt: remove consecutive blank lines - app/src/main/java/com/stormpanda/megingiard/MainAppScreen.kt: remove consecutive blank lines - domain/src/test/java/com/stormpanda/megingiard/mirror/ScreenCaptureFollowTest.kt: add unit test verifying follow animation cancellation on cutout target switch
Comment on lines
71
to
+75
| internal var scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) | ||
| set(value) { | ||
| field = value | ||
| restartCollectors() | ||
| } |
Comment on lines
203
to
+210
| private suspend fun writeMacroPadDataNow() { | ||
| val profiles = MacroPadState.profiles.value | ||
| val activeId = MacroPadState.activeProfileId.value | ||
| val json = macropadJson.encodeToString(profiles) | ||
| lastLoadedProfilesJson = json | ||
| lastLoadedActiveProfileId = activeId | ||
| dataStore.edit { prefs -> | ||
| prefs[KEY_MACROPAD_PROFILES] = macropadJson.encodeToString(profiles) | ||
| prefs[KEY_MACROPAD_PROFILES] = json |
Comment on lines
+172
to
+175
| val srcRatio = srcWidth.toFloat() / srcHeight.toFloat() | ||
| val targetRatio = targetWidth.toFloat() / targetHeight.toFloat() | ||
|
|
||
| var finalWidth = targetWidth | ||
| var finalHeight = targetHeight | ||
|
|
||
| if (srcRatio > targetRatio) { | ||
| // Source is wider than target. Fit width, calculate height to maintain ratio. | ||
| finalHeight = (targetWidth / srcRatio).toInt() | ||
| } else { | ||
| // Source is taller than target. Fit height, calculate width. | ||
| finalWidth = (targetHeight * srcRatio).toInt() | ||
| } | ||
|
|
||
| ScreenCaptureManager.setSurfaceSize(finalWidth.toFloat(), finalHeight.toFloat()) | ||
| ScreenCaptureManager.setSurfaceSize(targetWidth.toFloat(), targetHeight.toFloat()) |
Comment on lines
+903
to
+909
| if (cutout.shape == CutoutShape.CIRCLE) { | ||
| val path = Path().apply { | ||
| val r = min(dw, dh) / 2f | ||
| addCircle(dw / 2f, dh / 2f, r, Path.Direction.CW) | ||
| } | ||
| canvas.clipPath(path) | ||
| } |
- cancel followAnimationJob and clear followAnimationCutoutId when scope changes in ScreenCaptureManager - assign lastLoadedProfilesJson and lastLoadedActiveProfileId cache variables after dataStore.edit transaction succeeds in MacroPadSettings - remove unused srcRatio and targetRatio from MirrorPresentation setup - cache and reuse a single circlePath instance inside MultiCutoutContainer to avoid per-frame allocations in MirrorPresentation - enable saving blank cutout names in the rename dialog inside BackgroundSettingsOverlay
Comment on lines
126
to
130
| isConsumed: Boolean, | ||
| ): Boolean { | ||
| if (gestureInEdgeZone || !gestureStarted) return false | ||
| if (pointerId != activePointerId) return false | ||
|
|
Comment on lines
+1000
to
+1005
| if (!masterViewDrawn && !isFrozen && masterView != null && cutouts.isNotEmpty()) { | ||
| val saveCount = canvas.save() | ||
| canvas.clipRect(DUMMY_CLIP_LEFT, DUMMY_CLIP_TOP, DUMMY_CLIP_RIGHT, DUMMY_CLIP_BOTTOM) | ||
| drawChild(canvas, masterView, drawTime) | ||
| canvas.restoreToCount(saveCount) | ||
| } |
Comment on lines
+72
to
+76
| .border( | ||
| width = PM_BORDER_WIDTH, | ||
| color = if (isCapturing) colors.accent.copy(alpha = 0.5f) else colors.onControlOverlay.copy(alpha = 0.15f), | ||
| shape = RoundedCornerShape(PM_ACTION_BUTTON_CORNER) | ||
| ) |
- check isConsumed in TouchProjectionController.onMove and inject UP event immediately to prevent stuck touches during gesture takeover - clip fallback draw path of master view to an empty 0x0 rect and remove DUMMY_CLIP_* constants in MirrorPresentation to prevent stray pixels leaking - highlight Screen Mirroring button background and border color in PillMirrorCard when isViewportEditActive is true
Comment on lines
+199
to
+201
| val fps = ScreenCaptureManager.maxFps.value | ||
| AppLog.i(TAG, "Setting initial surface frame rate to $fps FPS") | ||
| surface.setFrameRate(fps.toFloat(), Surface.FRAME_RATE_COMPATIBILITY_DEFAULT) |
Comment on lines
+245
to
+246
| AppLog.i(TAG, "Setting surface frame rate to $fps FPS") | ||
| surface.setFrameRate(fps.toFloat(), Surface.FRAME_RATE_COMPATIBILITY_DEFAULT) |
Comment on lines
130
to
132
| onStart = { | ||
| AppStateManager.requestMirrorStart() | ||
| onDismiss() | ||
| }, |
Comment on lines
+15
to
+17
| fun sendToDirectServer(surfaces: List<Triple<Surface, Int, Int>>): Boolean { | ||
| val validSurfaces = surfaces.filter { it.first.isValid } | ||
| if (validSurfaces.isEmpty()) { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.