diff --git a/dev/CameraApp-Refs.sln b/dev/CameraApp-Refs.sln index 879dfbe..cec0e71 100644 --- a/dev/CameraApp-Refs.sln +++ b/dev/CameraApp-Refs.sln @@ -21,11 +21,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawnUi.Maui", "..\..\Drawn EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShadersCamera", "..\src\app\ShadersCamera.csproj", "{9F71E169-6D24-B132-9826-8A6DA9FFFBC8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShadersCarouselDemo", "..\..\ShadersCarousel\src\demo\ShadersCarouselDemo.csproj", "{A53734E6-1896-F775-5EEA-1A175FDA2B28}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastRepro", "..\..\DrawnUi.Maui\src\Maui\Samples\FastRepro\FastRepro.csproj", "{AF62CF62-A472-E87B-7225-8BD178AC3DF2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sandbox", "..\..\DrawnUi.Maui\src\Maui\Samples\Sandbox\Sandbox.csproj", "{9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CameraTests", "..\..\DrawnUi.Maui\src\Maui\Samples\Camera\CameraTests.csproj", "{2C274321-F41E-090D-C929-79400D88D71E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -47,21 +43,10 @@ Global {9F71E169-6D24-B132-9826-8A6DA9FFFBC8}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F71E169-6D24-B132-9826-8A6DA9FFFBC8}.Release|Any CPU.Build.0 = Release|Any CPU {9F71E169-6D24-B132-9826-8A6DA9FFFBC8}.Release|Any CPU.Deploy.0 = Release|Any CPU - {A53734E6-1896-F775-5EEA-1A175FDA2B28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A53734E6-1896-F775-5EEA-1A175FDA2B28}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A53734E6-1896-F775-5EEA-1A175FDA2B28}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {A53734E6-1896-F775-5EEA-1A175FDA2B28}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A53734E6-1896-F775-5EEA-1A175FDA2B28}.Release|Any CPU.Build.0 = Release|Any CPU - {A53734E6-1896-F775-5EEA-1A175FDA2B28}.Release|Any CPU.Deploy.0 = Release|Any CPU - {AF62CF62-A472-E87B-7225-8BD178AC3DF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF62CF62-A472-E87B-7225-8BD178AC3DF2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF62CF62-A472-E87B-7225-8BD178AC3DF2}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {AF62CF62-A472-E87B-7225-8BD178AC3DF2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF62CF62-A472-E87B-7225-8BD178AC3DF2}.Release|Any CPU.Build.0 = Release|Any CPU - {9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF}.Release|Any CPU.Build.0 = Release|Any CPU + {2C274321-F41E-090D-C929-79400D88D71E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C274321-F41E-090D-C929-79400D88D71E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C274321-F41E-090D-C929-79400D88D71E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C274321-F41E-090D-C929-79400D88D71E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -70,9 +55,7 @@ Global {DD2D491D-7046-41D2-A00E-FE65CBADE85E} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} {83974207-9636-48DD-BDB3-98EDECBB1107} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} {93E119B1-4378-87DF-2DD2-A818D1E6C2A2} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} - {A53734E6-1896-F775-5EEA-1A175FDA2B28} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} - {AF62CF62-A472-E87B-7225-8BD178AC3DF2} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} - {9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} + {2C274321-F41E-090D-C929-79400D88D71E} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {329E3D0C-A3F7-4A3E-B61C-6B2D1BD7F708} diff --git a/src/app/Platforms/Android/AndroidManifest.xml b/src/app/Platforms/Android/AndroidManifest.xml index 5bcb200..9ac1420 100644 --- a/src/app/Platforms/Android/AndroidManifest.xml +++ b/src/app/Platforms/Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + diff --git a/src/app/Platforms/Android/MainActivity.cs b/src/app/Platforms/Android/MainActivity.cs index 7eb5b7a..afd90ab 100644 --- a/src/app/Platforms/Android/MainActivity.cs +++ b/src/app/Platforms/Android/MainActivity.cs @@ -4,7 +4,7 @@ namespace ShadersCamera { [Activity(Theme = "@style/MainTheme", MainLauncher = true, - LaunchMode = LaunchMode.SingleTask, + LaunchMode = LaunchMode.SingleInstance, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode diff --git a/src/app/Platforms/iOS/AppDelegate.cs b/src/app/Platforms/iOS/AppDelegate.cs index a5bc494..25c1943 100644 --- a/src/app/Platforms/iOS/AppDelegate.cs +++ b/src/app/Platforms/iOS/AppDelegate.cs @@ -1,4 +1,5 @@ using Foundation; +using UIKit; namespace ShadersCamera { @@ -6,5 +7,13 @@ namespace ShadersCamera public class AppDelegate : MauiUIApplicationDelegate { protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + + /* + [Export("application:supportedInterfaceOrientationsForWindow:")] + public UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow) + { + return UIInterfaceOrientationMask.Portrait; + } + */ } } \ No newline at end of file diff --git a/src/app/Platforms/iOS/Info.plist b/src/app/Platforms/iOS/Info.plist index 7a94a8c..38473ad 100644 --- a/src/app/Platforms/iOS/Info.plist +++ b/src/app/Platforms/iOS/Info.plist @@ -3,7 +3,7 @@ CFBundleVersion - 141002 + 151004 LSRequiresIPhoneOS UIDeviceFamily @@ -14,13 +14,14 @@ XSAppIconAssets Assets.xcassets/icon.appiconset NSCameraUsageDescription - This app uses the camera to capture and process photos. + We need camera access to capture and process photos. NSPhotoLibraryAddUsageDescription - This app saves photos you take to library. + We need access to your photo library to save photos you take. NSPhotoLibraryUsageDescription - This app saves photos you take to library. + We need access to your photo library to save photos you take. NSLocationWhenInUseUsageDescription This app allows you to save geolocation data to photo metadata. + UIRequiredDeviceCapabilities arm64 diff --git a/src/app/Resources/Strings/ResStrings.resx b/src/app/Resources/Strings/ResStrings.resx index 8faa155..65afb4b 100644 --- a/src/app/Resources/Strings/ResStrings.resx +++ b/src/app/Resources/Strings/ResStrings.resx @@ -163,6 +163,9 @@ - Tap anywhere on the screen to set frame as preview - Choose your filter from bottom drawer menu! - Can zoom camera with fingers +- Tap shutter to take a photo +- Tap PHOTO/VIDEO label to switch mode +- In VIDEO mode, tap shutter to start/stop recording - Open settings for more! diff --git a/src/app/ShadersCamera.csproj b/src/app/ShadersCamera.csproj index 57c44ee..b9a7e46 100644 --- a/src/app/ShadersCamera.csproj +++ b/src/app/ShadersCamera.csproj @@ -3,10 +3,10 @@ - + net9.0-android; - net9.0-android;net9.0-ios;net9.0-maccatalyst - $(TargetFrameworks);net9.0-windows10.0.19041.0 + Exe ShadersCamera @@ -63,6 +63,11 @@ + + manual + Apple Distribution: NIKOLAY KOVALSKIY (F5H2D34D9G) + ShadersCam AppStore + @@ -96,7 +101,7 @@ --> - + @@ -151,6 +156,11 @@ + + + + + diff --git a/src/app/ViewModels/CameraViewModel.cs b/src/app/ViewModels/CameraViewModel.cs index 23e8d93..05ec633 100644 --- a/src/app/ViewModels/CameraViewModel.cs +++ b/src/app/ViewModels/CameraViewModel.cs @@ -2,383 +2,428 @@ using DrawnUi.Camera; using ShadersCamera.Helpers; using ShadersCamera.Models; -using System; using System.Collections.ObjectModel; +using System.Threading; using System.Windows.Input; -namespace ShadersCamera.ViewModels +namespace ShadersCamera.ViewModels; + +/// +/// Captured picture will land here. +/// We can get a preview for AI analysis inside this viewmodel too. +/// Could pass the captured image further using callback. +/// +public class CameraViewModel : ProjectViewModel, IQueryAttributable { - /// - /// Captured picture will land here. - /// We can get a preview for AI analysis inside this viewmodel too. - /// Could pass the captured image further using callback. - /// - public class CameraViewModel : ProjectViewModel, IQueryAttributable + public enum CaptureUIMode { - public CameraViewModel() + Photo = 0, + Video = 1 + } + + public CameraViewModel() + { + ShaderItems = new ObservableCollection { - ShaderItems = new ObservableCollection - { - new ShaderItem { Title = "Classic", Filename = "Shaders/Camera/bwclassic.sksl" }, - new ShaderItem { Title = "Street", Filename = "Shaders/Camera/bwstreet.sksl" }, - new ShaderItem { Title = "Street Zoom", Filename = "Shaders/Camera/bwstreet200.sksl" }, - new ShaderItem { Title = "Fine Art", Filename = "Shaders/Camera/bwfineart.sksl" }, - new ShaderItem { Title = "Kodak", Filename = "Shaders/Camera/kodaktmax400.sksl" }, - new ShaderItem { Title = "Fuji", Filename = "Shaders/Camera/fujineopan400.sksl" }, - new ShaderItem { Title = "Ilford", Filename = "Shaders/Camera/ilford.sksl" }, - new ShaderItem { Title = "Newspaper", Filename = "Shaders/Camera/bwprint.sksl" }, - new ShaderItem { Title = "Sin City", Filename = "Shaders/Camera/selective.sksl" }, - - new ShaderItem { Title = "Raw", Filename = "Shaders/Camera/blit.sksl" }, - new ShaderItem { Title = "Zoom", Filename = "Shaders/Camera/photozoom.sksl" }, - new ShaderItem { Title = "Desaturated", Filename = "Shaders/Camera/snyder.sksl" }, - - new ShaderItem { Title = "Romance", Filename = "Shaders/Camera/romance.sksl" }, - new ShaderItem { Title = "SoftPink", Filename = "Shaders/Camera/faded.sksl" }, - new ShaderItem { Title = "Soft Orange", Filename = "Shaders/Camera/insta.sksl" }, - new ShaderItem { Title = "Soft", Filename = "Shaders/Camera/orange.sksl" }, - new ShaderItem { Title = "Wes", Filename = "Shaders/Camera/wes.sksl" }, - - new ShaderItem { Title = "Action", Filename = "Shaders/Camera/action.sksl" }, - new ShaderItem { Title = "Movie", Filename = "Shaders/Camera/film.sksl" }, - - new ShaderItem { Title = "Mystic", Filename = "Shaders/Camera/enigma.sksl" }, - new ShaderItem { Title = "Blues", Filename = "Shaders/Camera/nolan.sksl" }, - new ShaderItem { Title = "Runner", Filename = "Shaders/Camera/blade.sksl" }, - new ShaderItem { Title = "Party", Filename = "Shaders/Camera/pink.sksl" }, - - new ShaderItem { Title = "Desert", Filename = "Shaders/Camera/desert.sksl" }, - new ShaderItem { Title = "Blockbuster", Filename = "Shaders/Camera/blockbuster.sksl" }, - new ShaderItem { Title = "Kodachrome", Filename = "Shaders/Camera/kodachrome.sksl" }, - - //new ShaderItem { Title = "Palette", Filename = "Shaders/Camera/old-palette.sksl" }, - new ShaderItem { Title = "TV", Filename = "Shaders/Camera/retrotv.sksl" }, - new ShaderItem { Title = "Pixels", Filename = "Shaders/Camera/pixels.sksl" }, - - //new ShaderItem { Title = "Gel", Filename = "Shaders/Camera/liquidglass.sksl" }, - - new ShaderItem { Title = "Sketch", Filename = "Shaders/Camera/sketch.sksl" }, - new ShaderItem { Title = "Paint", Filename = "Shaders/Camera/sketchcolors.sksl" }, + new ShaderItem { Title = "Classic", Filename = "Shaders/Camera/bwclassic.sksl" }, + new ShaderItem { Title = "Street", Filename = "Shaders/Camera/bwstreet.sksl" }, + new ShaderItem { Title = "Street Zoom", Filename = "Shaders/Camera/bwstreet200.sksl" }, + new ShaderItem { Title = "Fine Art", Filename = "Shaders/Camera/bwfineart.sksl" }, + new ShaderItem { Title = "Kodak", Filename = "Shaders/Camera/kodaktmax400.sksl" }, + new ShaderItem { Title = "Fuji", Filename = "Shaders/Camera/fujineopan400.sksl" }, + new ShaderItem { Title = "Ilford", Filename = "Shaders/Camera/ilford.sksl" }, + new ShaderItem { Title = "Newspaper", Filename = "Shaders/Camera/bwprint.sksl" }, + new ShaderItem { Title = "Sin City", Filename = "Shaders/Camera/selective.sksl" }, + + new ShaderItem { Title = "Raw", Filename = "Shaders/Camera/blit.sksl" }, + new ShaderItem { Title = "Zoom", Filename = "Shaders/Camera/photozoom.sksl" }, + new ShaderItem { Title = "Desaturated", Filename = "Shaders/Camera/snyder.sksl" }, + + new ShaderItem { Title = "Romance", Filename = "Shaders/Camera/romance.sksl" }, + new ShaderItem { Title = "SoftPink", Filename = "Shaders/Camera/faded.sksl" }, + new ShaderItem { Title = "Soft Orange", Filename = "Shaders/Camera/insta.sksl" }, + new ShaderItem { Title = "Soft", Filename = "Shaders/Camera/orange.sksl" }, + new ShaderItem { Title = "Wes", Filename = "Shaders/Camera/wes.sksl" }, + + new ShaderItem { Title = "Action", Filename = "Shaders/Camera/action.sksl" }, + new ShaderItem { Title = "Movie", Filename = "Shaders/Camera/film.sksl" }, + + new ShaderItem { Title = "Mystic", Filename = "Shaders/Camera/enigma.sksl" }, + new ShaderItem { Title = "Blues", Filename = "Shaders/Camera/nolan.sksl" }, + new ShaderItem { Title = "Runner", Filename = "Shaders/Camera/blade.sksl" }, + new ShaderItem { Title = "Party", Filename = "Shaders/Camera/pink.sksl" }, + + new ShaderItem { Title = "Desert", Filename = "Shaders/Camera/desert.sksl" }, + new ShaderItem { Title = "Blockbuster", Filename = "Shaders/Camera/blockbuster.sksl" }, + new ShaderItem { Title = "Kodachrome", Filename = "Shaders/Camera/kodachrome.sksl" }, + + new ShaderItem { Title = "TV", Filename = "Shaders/Camera/retrotv.sksl" }, + new ShaderItem { Title = "Pixels", Filename = "Shaders/Camera/pixels.sksl" }, + + new ShaderItem { Title = "Sketch", Filename = "Shaders/Camera/sketch.sksl" }, + new ShaderItem { Title = "Paint", Filename = "Shaders/Camera/sketchcolors.sksl" }, #if !ANDROID - new ShaderItem { Title = "Poster", Filename = "Shaders/Camera/sketchcomics4.sksl" }, + new ShaderItem { Title = "Poster", Filename = "Shaders/Camera/sketchcomics4.sksl" }, #endif - new ShaderItem { Title = "Mars", Filename = "Shaders/Camera/hell.sksl" }, - new ShaderItem { Title = "Invert", Filename = "Shaders/Camera/invert.sksl" }, - new ShaderItem { Title = "Negative", Filename = "Shaders/Camera/negative.sksl" }, - }; + new ShaderItem { Title = "Mars", Filename = "Shaders/Camera/hell.sksl" }, + new ShaderItem { Title = "Invert", Filename = "Shaders/Camera/invert.sksl" }, + new ShaderItem { Title = "Negative", Filename = "Shaders/Camera/negative.sksl" }, + }; - var index = 0; - if (!string.IsNullOrEmpty(UserSettings.Current.Filter)) + var index = 0; + if (!string.IsNullOrEmpty(UserSettings.Current.Filter)) + { + var shaderNb = 0; + foreach (var shader in ShaderItems) { - var shaderNb = 0; - foreach (var shader in ShaderItems) + if (shader.Title == UserSettings.Current.Filter) { - if (shader.Title == UserSettings.Current.Filter) - { - index = shaderNb; - } - - shaderNb++; + index = shaderNb; } + + shaderNb++; } + } - SelectedShaderIndex = index; - - InitialIndex = index; //initial scroll position + SelectedShaderIndex = index; + InitialIndex = index; + + Mode = CaptureUIMode.Photo; + } + + public int InitialIndex + { + get => _initialIndex; + set + { + if (value == _initialIndex) return; + _initialIndex = value; + OnPropertyChanged(); } + } - public int InitialIndex + public int SelectedShaderIndex + { + get => _selectedShaderIndex; + set { - get => _initialIndex; - set + if (value == _selectedShaderIndex) return; + _selectedShaderIndex = value; + OnPropertyChanged(); + if (value >= 0) { - if (value == _initialIndex) return; - _initialIndex = value; - OnPropertyChanged(); + SelectedShader = ShaderItems[value]; } } + } - public int SelectedShaderIndex + public void ApplyQueryAttributes(IDictionary query) + { + if (query != null) { - get => _selectedShaderIndex; - set + query.TryGetValue("callback", out var command); + if (command != null) { - if (value == _selectedShaderIndex) return; - _selectedShaderIndex = value; - OnPropertyChanged(); - if (value >= 0) - { - SelectedShader = ShaderItems[value]; - } + Callback = command as ICommand; } } + } - public void ApplyQueryAttributes(IDictionary query) + #region PROPERTIES + + public ObservableCollection ShaderItems + { + get => _shaderItems; + set { - if (query != null) + if (_shaderItems != value) { - query.TryGetValue("callback", out var command); - if (command != null) - { - Callback = command as ICommand; - } + _shaderItems = value; + OnPropertyChanged(); } } + } - #region PROPERTIES - - private ObservableCollection _shaderItems; - - public ObservableCollection ShaderItems + public ShaderItem SelectedShader + { + get => _selectedShader; + set { - get { return _shaderItems; } - set + if (_selectedShader != value) { - if (_shaderItems != value) - { - _shaderItems = value; - OnPropertyChanged(); - } + _selectedShader = value; + OnPropertyChanged(); + + UserSettings.Current.Filter = value?.Title ?? string.Empty; + UserSettings.Save(); } } + } - private ShaderItem _selectedShader; + public ICommand Callback { get; set; } - public ShaderItem SelectedShader + public LoadedImageSource DisplayPreview + { + get => _displayPreview; + set { - get { return _selectedShader; } - set + if (_displayPreview != value) { - if (_selectedShader != value) - { - _selectedShader = value; - OnPropertyChanged(); - if (value == null) - { - UserSettings.Current.Filter = string.Empty; - } - else - { - UserSettings.Current.Filter = value.Title; - } - - UserSettings.Save(); - } + _displayPreview = value; + OnPropertyChanged(); } } + } - #endregion - - #region COMMANDS - - public ICommand CommandSelectShader + public ImageSource LoadImage + { + get => _loadImage; + set { - get + if (_loadImage != value) { - return new Command(async (object context) => - { - if (context is ShaderItem shader) - { - SelectedShader = shader; - SelectedShaderIndex = ShaderItems.IndexOf(shader); - } - }); + _loadImage = value; + OnPropertyChanged(); } } + } - - public ICommand CommandCaptureStillPhoto + public bool ShowResume + { + get => _showResume; + set { - get + if (_showResume != value) { - return new Command((object context) => - { - if (TouchEffect.CheckLockAndSet()) - return; - - if (Camera.State == CameraState.On && !Camera.IsBusy) - { - //Camera.FlashScreen(Color.Parse("#EE000000")); - _ = Camera.TakePicture().ConfigureAwait(false); - } - }); + _showResume = value; + OnPropertyChanged(); } } + } + + public CaptureUIMode Mode + { + get => _mode; + set + { + if (value == _mode) return; + _mode = value; + OnPropertyChanged(); + } + } - public Command CommandPreviewTapped => new Command(async () => + public TimeSpan RecordingDuration + { + get => _recordingDuration; + set { - if (TouchEffect.CheckLockAndSet() || string.IsNullOrEmpty(_lastSavedPath)) - return; + if (value == _recordingDuration) return; + _recordingDuration = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(RecordingDurationText)); + } + } - MainThread.BeginInvokeOnMainThread(() => { SkiaCamera.OpenFileInGallery(_lastSavedPath); }); - }); + public string RecordingDurationText => RecordingDuration.ToString(@"mm\:ss"); - #endregion + public bool IsRecording => Camera?.IsRecording ?? false; - /// - /// Cat be set from arguments by shell - /// - public ICommand Callback { get; set; } + #endregion - private LoadedImageSource _displayPreview; + #region COMMANDS - public LoadedImageSource DisplayPreview + public ICommand CommandSelectShader => new Command((object context) => + { + if (context is ShaderItem shader) { - get { return _displayPreview; } - set - { - if (_displayPreview != value) - { - _displayPreview = value; - OnPropertyChanged(); - } - } + SelectedShader = shader; + SelectedShaderIndex = ShaderItems.IndexOf(shader); } + }); - private ImageSource _LoadImage; + public ICommand CommandCaptureStillPhoto => new Command((object context) => + { + if (TouchEffect.CheckLockAndSet()) + return; - public ImageSource LoadImage + if (Camera?.State == CameraState.On && !Camera.IsBusy) { - get { return _LoadImage; } - set - { - if (_LoadImage != value) - { - _LoadImage = value; - OnPropertyChanged(); - } - } + _ = Camera.TakePicture().ConfigureAwait(false); } + }); + + public Command CommandPreviewTapped => new Command(() => + { + if (TouchEffect.CheckLockAndSet() || string.IsNullOrEmpty(_lastSavedPath)) + return; - SemaphoreSlim semaphoreProcessingFrame = new(1, 1); + MainThread.BeginInvokeOnMainThread(() => { SkiaCamera.OpenFileInGallery(_lastSavedPath); }); + }); - private string _lastSavedPath; + public ICommand CommandShutterTapped => new Command(async () => + { + if (TouchEffect.CheckLockAndSet()) + return; - public void AttachCamera(SkiaCamera camera) + if (Camera?.State != CameraState.On || Camera.IsBusy) + return; + + if (Mode == CaptureUIMode.Photo) { - if (Camera == null && camera != null) - { - Camera = camera; - Camera.CaptureSuccess += OnCaptureSuccess; - Camera.StateChanged += OnCameraStateChanged; - Camera.NewPreviewSet += OnNewPreviewSet; - } + CommandCaptureStillPhoto.Execute(null); + return; } - public override void OnDisposing() + // Video mode: tap toggles start/stop + if (!Camera.CanRecordVideo) + return; + + try { - if (Camera != null) + if (Camera.IsRecording) { - Camera.CaptureSuccess -= OnCaptureSuccess; - Camera.StateChanged -= OnCameraStateChanged; - Camera.NewPreviewSet -= OnNewPreviewSet; - Camera = null; + await Camera.StopVideoRecording().ConfigureAwait(false); } + else + { + RecordingDuration = TimeSpan.Zero; + await Camera.StartVideoRecording().ConfigureAwait(false); + } + } + catch + { + // Caller can add user-facing error handling later. + } + }); + + public ICommand CommandToggleCaptureMode => new Command(() => + { + if (Camera?.IsRecording == true) + return; - base.OnDisposing(); + Mode = Mode == CaptureUIMode.Photo ? CaptureUIMode.Video : CaptureUIMode.Photo; + }); + + #endregion + + public void AttachCamera(SkiaCamera camera) + { + if (Camera == null && camera != null) + { + Camera = camera; + Camera.CaptureSuccess += OnCaptureSuccess; + Camera.StateChanged += OnCameraStateChanged; + Camera.NewPreviewSet += OnNewPreviewSet; + Camera.IsRecordingVideoChanged += OnIsRecordingVideoChanged; + Camera.RecordingProgress += OnVideoRecordingProgress; } + } - private void OnNewPreviewSet(object sender, LoadedImageSource source) + public override void OnDisposing() + { + if (Camera != null) { - //Task.Run(async () => - //{ + Camera.CaptureSuccess -= OnCaptureSuccess; + Camera.StateChanged -= OnCameraStateChanged; + Camera.NewPreviewSet -= OnNewPreviewSet; + Camera.IsRecordingVideoChanged -= OnIsRecordingVideoChanged; + Camera.RecordingProgress -= OnVideoRecordingProgress; + Camera = null; + } - // ProcessPreviewFrame(source); was used in detection mode + base.OnDisposing(); + } - //}).ConfigureAwait(false); + private void OnIsRecordingVideoChanged(object sender, bool isRecording) + { + OnPropertyChanged(nameof(IsRecording)); + if (!isRecording) + { + RecordingDuration = TimeSpan.Zero; } + } - private void OnCameraStateChanged(object sender, CameraState state) - { - if (state == CameraState.On) - { - if (Camera != null && Camera.Display != null) - { - Camera.Display.Blur = 0; - } + private void OnVideoRecordingProgress(object sender, TimeSpan duration) + { + RecordingDuration = duration; + } - ShowResume = false; - } - else + private void OnNewPreviewSet(object sender, LoadedImageSource source) + { + // was used in detection mode + } + + private void OnCameraStateChanged(object sender, CameraState state) + { + if (state == CameraState.On) + { + if (Camera?.Display != null) { - ShowResume = true; - if (Camera != null && Camera.Display != null) - { - Camera.Display.Blur = 10; - } + Camera.Display.Blur = 0; } - } - - private bool _ShowResume; - public bool ShowResume + ShowResume = false; + } + else { - get { return _ShowResume; } - set + ShowResume = true; + if (Camera?.Display != null) { - if (_ShowResume != value) - { - _ShowResume = value; - OnPropertyChanged(); - } + Camera.Display.Blur = 10; } } + } - bool _loadOnce; - private int _selectedShaderIndex = -1; - private int _initialIndex; - - protected SkiaCamera Camera { get; set; } + private async void OnCaptureSuccess(object sender, CapturedImage captured) + { + captured.SolveExifOrientation(); - private async void OnCaptureSuccess(object sender, CapturedImage captured) + var imageWithOverlay = await Camera.RenderCapturedPhotoAsync(captured, null, image => { - captured.SolveExifOrientation(); - - var imageWithOverlay = await Camera.RenderCapturedPhotoAsync(captured, null, image => + if (SelectedShader != null) { - if (SelectedShader != null) + var shaderEffect = new SkiaShaderEffect() { - var shaderEffect = new SkiaShaderEffect() - { - ShaderSource = SelectedShader.Filename, - TileMode = SKShaderTileMode.Mirror - }; - image.VisualEffects.Add(shaderEffect); - } - }, true); - - //going to use the newly created bitmap with effects - //to save to gallery, so need to dispose the original one - captured.Image.Dispose(); - captured.Image = imageWithOverlay; + ShaderSource = SelectedShader.Filename, + TileMode = SKShaderTileMode.Mirror + }; + image.VisualEffects.Add(shaderEffect); + } + }, true); - captured.Meta.Vendor = MauiProgram.ExifCameraVendor; - captured.Meta.Model = MauiProgram.ExifCameraModel; + captured.Image.Dispose(); + captured.Image = imageWithOverlay; - //save to device, can use custom album name if needed - await Camera.SaveToGalleryAsync(captured); + captured.Meta.Vendor = MauiProgram.ExifCameraVendor; + captured.Meta.Model = MauiProgram.ExifCameraModel; - //display preview - //captured.Bitmap will be disposed by image ImagePreview when it - //changes source to a new one, or when ImagePreview is disposed - //ImagePreview.SetBitmapInternal(captured.Bitmap); - //DisplayPreview = new(captured.Image); //not using this in this screen + await Camera.SaveToGalleryAsync(captured); - _lastSavedPath = captured.Path; + _lastSavedPath = captured.Path; - if (Callback != null) - { - Callback.Execute(captured); - } + Callback?.Execute(captured); - var dispose = DisplayPreview; - DisplayPreview = new LoadedImageSource(captured.Image); - if (dispose != null) - { - Camera?.DisposeObject(dispose); - } + var dispose = DisplayPreview; + DisplayPreview = new LoadedImageSource(captured.Image); + if (dispose != null) + { + Camera?.DisposeObject(dispose); } } + + private ObservableCollection _shaderItems; + private ShaderItem _selectedShader; + private LoadedImageSource _displayPreview; + private ImageSource _loadImage; + private bool _showResume; + private int _selectedShaderIndex = -1; + private int _initialIndex; + private string _lastSavedPath; + private CaptureUIMode _mode; + private TimeSpan _recordingDuration; + + private readonly SemaphoreSlim semaphoreProcessingFrame = new(1, 1); + protected SkiaCamera Camera { get; set; } } \ No newline at end of file diff --git a/src/app/Views/Controls/CameraWithEffects.cs b/src/app/Views/Controls/CameraWithEffects.cs index 118e888..2e0a3a8 100644 --- a/src/app/Views/Controls/CameraWithEffects.cs +++ b/src/app/Views/Controls/CameraWithEffects.cs @@ -11,7 +11,7 @@ public class CameraWithEffects : SkiaCamera public CameraWithEffects() { - + //NeedPermissionsSet = NeedPermissions.Camera | NeedPermissions.Gallery; } protected override void OnDisplayReady() diff --git a/src/app/Views/MainCameraPage.xaml b/src/app/Views/MainCameraPage.xaml deleted file mode 100644 index 4fd4e35..0000000 --- a/src/app/Views/MainCameraPage.xaml +++ /dev/null @@ -1,431 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/app/Views/MainCameraPage.xaml.cs b/src/app/Views/MainCameraPage.xaml.cs deleted file mode 100644 index d6f4935..0000000 --- a/src/app/Views/MainCameraPage.xaml.cs +++ /dev/null @@ -1,551 +0,0 @@ -using DrawnUi.Camera; -using FastPopups; -using Newtonsoft.Json; -using ShadersCamera.ViewModels; -using System.ComponentModel; -using System.Diagnostics; -using AppoMobi.Specials; -using ShadersCamera.Helpers; -using ShadersCamera.Resources.Strings; - -namespace ShadersCamera.Views; - -public partial class MainCameraPage : IPageWIthCamera, IDisposable -{ - public MainCameraPage() - { - try - { - _vm = new CameraViewModel(); - - BindingContext = _vm; - - IsFullScreen = UserSettings.Current.Fill; - IsMirrored = UserSettings.Current.Mirror; - - InitializeComponent(); - - CameraControl.CaptureFlashMode = (CaptureFlashMode)UserSettings.Current.Flash; - - CameraControl.PropertyChanged += OnContextPropertyChanged; - -#if ANDROID - Super.SetNavigationBarColor(Colors.Black, Colors.Black, true); -#endif - } - catch (Exception e) - { - Super.DisplayException(this, e); - } - } - - - void AttachCamera() - { - if (BindingContext is CameraViewModel vm) - { - vm.AttachCamera(CameraControl); - - CameraControl.NewPreviewSet += OnPreviewSet; - CameraControl.StateChanged += OnCameraStateChanged; - - SyncUi(); - - try - { - CameraControl.IsOn = true; - } - catch (Exception e) - { - Super.Log(e); - } - } - } - - - /// - /// Flag to capture a small ML/other small image from the current camera preview - /// - private bool TriggerUpdateSmallPreview; - - SemaphoreSlim semaphoreProcessingFrame = new(1, 1); - private object lockCatchFrame = new(); - - /// - /// Flag to check first ever preview frame received ok - /// - private bool StartupSuccessChecked; - - private void OnCameraStateChanged(object sender, CameraState state) - { - if (state == CameraState.On) - { - Debug.WriteLine($"[CameraApp] State in ON!"); - - if (UserSettings.Current.Formats.TryGetValue(CameraControl.CameraDevice.Id, out var format)) - { - CameraControl.PhotoFormatIndex = format; - CameraControl.PhotoQuality = CaptureQuality.Manual; - } - else - { - CameraControl.PhotoQuality = CaptureQuality.Medium; - } - } - } - - public void OpenHelp() - { - MainThread.BeginInvokeOnMainThread(() => - { - var popup = new HelpPopup(); - this.ShowPopup(popup); - UserSettings.Save(); - }); - } - - - /// - /// We have captured a preview frame. - /// Will use it for shaders menu. Same mechanics could be used to send this to AI etc. - /// - /// - /// - private void OnPreviewSet(object sender, LoadedImageSource source) - { - lock (lockCatchFrame) - { - if (!StartupSuccessChecked) - { - StartupSuccessChecked = true; - try - { - if (!UserSettings.Current.ShownWelcome) - { - UserSettings.Current.ShownWelcome = true; - OpenHelp(); - } - } - catch (Exception e) - { - Super.Log(e); - } - - return; - } - - if (TriggerUpdateSmallPreview && semaphoreProcessingFrame.CurrentCount != 0) - { - TriggerUpdateSmallPreview = false; - - var image = source.Image; - if (image == null) - { - image = SKImage.FromBitmap(source.Bitmap); - } - - //full copy from gpu preview surface NOT USED - //var info = image.Info; - //var pixelData = new byte[info.BytesSize]; - //unsafe - //{ - // fixed (byte* ptr = pixelData) - // { - // bool success = image.ReadPixels(info, (nint)ptr); - // if (success) - // { - // var copiedImage = SKImage.FromPixels(info, (nint)ptr, info.RowBytes); - // } - // } - //} - - var marginFactor = 0.2f; // Crop from sides to focus on core center - var targetSize = 256; - - var originalInfo = image.Info; - var maxCropSize = Math.Min(originalInfo.Width, originalInfo.Height); - var actualCropSize = maxCropSize * (1.0f - marginFactor); - - var cropX = (originalInfo.Width - actualCropSize) / 2; - var cropY = (originalInfo.Height - actualCropSize) / 2; - - var newInfo = new SKImageInfo(targetSize, targetSize, SKColorType.Rgb888x, SKAlphaType.Opaque); - using var surface = SKSurface.Create(newInfo); - surface.Canvas.DrawImage(image, - new SKRect(cropX, cropY, cropX + actualCropSize, cropY + actualCropSize), - new SKRect(0, 0, targetSize, targetSize)); - var mlImage = surface.Snapshot(); - - //use in UI - our preview for shaders menu - var dispose = SmallPreview; - SmallPreview = new LoadedImageSource(mlImage) - { - ProtectFromDispose = true - }; - if (dispose != null) - { - CameraControl.DisposeObject(dispose); - } - - //for AI/ML use this: - //Task.Run(async () => - //{ - // //todo use image here - - //}).ConfigureAwait(false); - } - } - } - - private LoadedImageSource _displayPreview; - - public LoadedImageSource SmallPreview - { - get { return _displayPreview; } - set - { - if (_displayPreview != value) - { - _displayPreview = value; - OnPropertyChanged(); - } - } - } - - - private readonly CameraViewModel _vm; - - - private void TappedSwitchCamera(object sender, ControlTappedEventArgs controlTappedEventArgs) - { - if (CameraControl.IsOn) - { - CameraControl.Facing = CameraControl.Facing == CameraPosition.Selfie - ? CameraPosition.Default - : CameraPosition.Selfie; - } - } - - private void TappedTurnCamera(object sender, ControlTappedEventArgs controlTappedEventArgs) - { - if (CameraControl.State == CameraState.On) - { - CameraControl.IsOn = false; - } - else - { - CameraControl.IsOn = true; - } - } - - /// - /// Cycle through effects in order: Sepia -> BlackAndWhite -> Pastel -> None -> Sepia... - /// - private void TappedCycleEffects(object sender, ControlTappedEventArgs controlTappedEventArgs) - { - var current = CameraControl.Effect; - var currentIndex = CameraControl.AvailableEffects.IndexOf(current); - - // Move to next effect, wrap around to beginning if at end - var nextIndex = (currentIndex + 1) % CameraControl.AvailableEffects.Count; - - CameraControl.SetEffect(CameraControl.AvailableEffects[nextIndex]); - } - - private async void TappedTakePicture(object sender, SkiaGesturesParameters skiaGesturesParameters) - { - if (CameraControl.State == CameraState.On && !CameraControl.IsBusy) - { - CameraControl.FlashScreen(Color.Parse("#EEFFFFFF")); - await CameraControl.TakePicture().ConfigureAwait(false); - } - } - - private void TappedResume(object sender, ControlTappedEventArgs controlTappedEventArgs) - { - CameraControl.IsOn = true; - } - - float step = 0.2f; - private bool _flashOn; - - - private void Tapped_ZoomOut(object sender, SkiaGesturesParameters skiaGesturesParameters) - { - CameraControl.Zoom -= step; - } - - private void Tapped_ZoomIn(object sender, SkiaGesturesParameters skiaGesturesParameters) - { - CameraControl.Zoom += step; - } - - private void OnZoomed(object sender, ZoomEventArgs e) - { - CameraControl.Zoom = e.Value; - } - - private void TappedFlash(object sender, ControlTappedEventArgs e) - { - _flashOn = !_flashOn; - - if (_flashOn) - { - CameraControl.FlashMode = FlashMode.On; - } - else - { - CameraControl.FlashMode = FlashMode.Off; - } - - SyncUi(); - } - - private void TappedBackground(object sender, ControlTappedEventArgs e) - { - TriggerUpdateSmallPreview = true; - } - - - private void WillFirstTimeDraw(object sender, SkiaDrawingContext e) - { - AttachCamera(); - } - - private void CanvasWillDispose(object sender, EventArgs e) - { - CameraControl.NewPreviewSet -= OnPreviewSet; - CameraControl.StateChanged -= OnCameraStateChanged; - } - - private void TappedDrawerHeader(object sender, ControlTappedEventArgs e) - { - ShaderDrawer.IsOpen = !ShaderDrawer.IsOpen; - } - - - /// - /// Observing SkiaCamera props - /// - /// - /// - private void OnContextPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(SkiaCamera.IsBusy)) - { - var color = CameraControl.IsBusy ? Colors.DarkRed : Color.Parse("#CECECE"); - ButtonCapture.BackgroundColor = color; - } - } - - public void Dispose() - { - CameraControl.PropertyChanged -= OnContextPropertyChanged; - } - - - #region SELECT FORMAT - - public CaptureFormat SelectedFormat - { - get { return CameraControl.CurrentStillCaptureFormat; } - } - - public void SelectFormat(Action changed) - { - if (CameraControl.IsOn) - { - MainThread.BeginInvokeOnMainThread(async () => - { - try - { - var formats = await CameraControl.GetAvailableCaptureFormatsAsync(); - - if (!formats.Any()) - { - await App.Current.MainPage.DisplayAlert("Error", "No capture formats available", "OK"); - return; - } - - // Create picker with detailed format info - var options = formats.Select((format, index) => - $"{format.Width}x{format.Height}, {format.AspectRatioString}" - ).ToArray(); - - var result = await App.Current.MainPage.DisplayActionSheet( - "Capture Photo Quality", - ResStrings.BtnCancel, - null, - options); - - if (!string.IsNullOrEmpty(result)) - { - var selectedIndex = Array.IndexOf(options, result); - if (selectedIndex >= 0) - { - // Set manual capture mode with selected format - CameraControl.PhotoFormatIndex = selectedIndex; - CameraControl.PhotoQuality = CaptureQuality.Manual; - OnPropertyChanged(nameof(SelectedFormat)); - changed?.Invoke(result); - - Debug.WriteLine( - $"[CameraApp] Format selection: {selectedIndex} for {CameraControl.CameraDevice.Id}"); - - UserSettings.Current.Formats[CameraControl.CameraDevice.Id] = selectedIndex; - } - } - } - catch (Exception ex) - { - await App.Current.MainPage.DisplayAlert("Error", $"Failed to get capture formats: {ex.Message}", - "OK"); - Debug.WriteLine($"[CameraApp] Format selection error: {ex}"); - } - }); - } - } - - #endregion - - #region SELECT ASPECT - - public void SetAspect(bool fullScreen) - { - IsFullScreen = fullScreen; - ApplyAspect(); - } - - void ApplyAspect() - { - if (CameraControl == null) - { - return; - } - - if (IsFullScreen) - { - CameraControl.Aspect = TransformAspect.AspectCover; - } - else - { - CameraControl.Aspect = TransformAspect.AspectFitFill; - } - - CameraControl.IsMirrored = IsMirrored; - - UserSettings.Current.Mirror = IsMirrored; - UserSettings.Current.Fill = CameraControl.Aspect == TransformAspect.AspectCover; - } - - public bool IsFullScreen { get; set; } - - public bool IsMirrored { get; set; } - - public void SetMirrored(bool value) - { - IsMirrored = value; - ApplyAspect(); - } - - #endregion - - void SyncUi() - { - ApplyAspect(); - - // CaptureFlashMode - var currentMode = CameraControl.CaptureFlashMode; - switch (currentMode) - { - case CaptureFlashMode.Off: - SvgFlashCapture.SvgString = App.Current.Resources.Get("SvgFlashOff"); - break; - case CaptureFlashMode.Auto: - SvgFlashCapture.SvgString = App.Current.Resources.Get("SvgFlashAuto"); - break; - case CaptureFlashMode.On: - SvgFlashCapture.SvgString = App.Current.Resources.Get("SvgFlashOn"); - break; - } - - //FlashMode - var torch = CameraControl.FlashMode; - switch (torch) - { - case FlashMode.On: - SvgFlashLight.SvgString = App.Current.Resources.Get("SvgLightOn"); - break; - case FlashMode.Off: - default: - SvgFlashLight.SvgString = App.Current.Resources.Get("SvgLightOff"); - break; - } - } - - private void OnFlashClicked(object sender, object e) - { - try - { - var currentMode = CameraControl.CaptureFlashMode; - var nextMode = currentMode switch - { - CaptureFlashMode.Off => CaptureFlashMode.Auto, - CaptureFlashMode.Auto => CaptureFlashMode.On, - CaptureFlashMode.On => CaptureFlashMode.Off, - _ => CaptureFlashMode.Auto - }; - - CameraControl.CaptureFlashMode = nextMode; - - SyncUi(); - - Debug.WriteLine($"Camera Status: Capture flash mode set to {nextMode}"); - - UserSettings.Current.Flash = (int)nextMode; - UserSettings.Save(); - } - catch (Exception ex) - { - Super.Log($"[CameraTestPage] OnFlashClicked error: {ex}"); - } - } - - private void TappedSettings(object sender, ControlTappedEventArgs args) - { - if (SelectedFormat == null || CameraControl.PermissionsError) - { - //camera error - try - { - CameraControl.IsOn = true; - } - catch (Exception e) - { - Super.Log(e); - } - - Tasks.StartDelayed(TimeSpan.FromMilliseconds(500), () => - { - if (CameraControl.PermissionsError) - { - MainThread.BeginInvokeOnMainThread(async () => - { - await DisplayAlert("Error", $"No permissions", "OK"); - -#if ANDROID || IOS - NativeTasks.OpenSystemSettings(); -#endif - }); - } - }); - return; - } - - MainThread.BeginInvokeOnMainThread(() => - { - var popup = new SettingsPopup(this); - this.ShowPopup(popup); - }); - } -} \ No newline at end of file diff --git a/src/app/Views/MainPageCameraFluent.Ui.cs b/src/app/Views/MainPageCameraFluent.Ui.cs index 8dbc8ef..5eb7168 100644 --- a/src/app/Views/MainPageCameraFluent.Ui.cs +++ b/src/app/Views/MainPageCameraFluent.Ui.cs @@ -25,6 +25,8 @@ public partial class MainCameraPageFluent : BasePageReloadable, IPageWIthCamera SkiaDrawer ShaderDrawer; SkiaImage ImagePreview; SkiaScroll MainScroll; + SkiaLabel LabelRecording; + SkiaLabel LabelCaptureMode; //will be called by page constructor and hotreload public override void Build() @@ -94,7 +96,8 @@ SkiaLayout CreateMainLayout() new SkiaDrawer() { - AutoCache = true, + AutoCache = false, + UseCache = SkiaCacheType.Operations, Margin = new Thickness(0, 0, 0, 100), HeaderSize = 40, Direction = DrawerDirection.FromLeft, @@ -123,7 +126,7 @@ SkiaLayout CreateMainLayout() { new SkiaScroll() { - //AutoCache = true, + AutoCache = true, BackgroundColor = Colors.WhiteSmoke, Margin = new Thickness(0, 0, 20, 0), Orientation = ScrollOrientation.Horizontal, @@ -144,7 +147,6 @@ SkiaLayout CreateMainLayout() }, Content = new SkiaLayoutWithSelector() { - UseCache = SkiaCacheType.Operations, Type = LayoutType.Row, VerticalOptions = LayoutOptions.Center, Spacing = 8, @@ -204,6 +206,7 @@ SkiaLayer CreateCameraLayer() Children = { CreateCameraControl(), + CreateRecordingBadge(), CreateControlsLayer(), CreateZoomHotspot() } @@ -211,6 +214,58 @@ SkiaLayer CreateCameraLayer() .OnTapped(me => { TriggerUpdateSmallPreview = true; }); } + SkiaShape CreateRecordingBadge() + { + return new SkiaShape() + { + ZIndex = 80, + Type = ShapeType.Rectangle, + CornerRadius = 12, + BackgroundColor = Color.Parse("#66000000"), + Padding = new Thickness(10, 6), + Margin = new Thickness(0, 56, 0, 0), + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Start, + IsVisible = false, + Children = + { + new SkiaRow() + { + Spacing = 8, + VerticalOptions = LayoutOptions.Center, + Children = + { + new SkiaShape() + { + Type = ShapeType.Circle, + BackgroundColor = Colors.DarkRed, + WidthRequest = 8, + HeightRequest = 8, + LockRatio = 1, + VerticalOptions = LayoutOptions.Center + }, + new SkiaLabel() + { + Text = "REC 00:00", + TextColor = Colors.White, + FontSize = 14, + VerticalOptions = LayoutOptions.Center + }.Assign(out LabelRecording) + } + } + } + } + .ObserveBindingContext((me, vm, prop) => + { + bool attached = prop == nameof(BindingContext); + if (attached || prop == nameof(vm.IsRecording) || prop == nameof(vm.RecordingDurationText)) + { + me.IsVisible = vm.IsRecording; + LabelRecording.Text = $"REC {vm.RecordingDurationText}"; + } + }); + } + CameraWithEffects CreateCameraControl() { return new CameraWithEffects() @@ -245,12 +300,57 @@ SkiaLayer CreateControlsLayer() VerticalOptions = LayoutOptions.Fill, Children = { + //CreateCaptureModeLabel(), //todo in next version for video CreateControlsPanel(), CreateResumeHotspot() } }; } + SkiaShape CreateCaptureModeLabel() + { + return new SkiaShape() + { + ZIndex = 70, + Type = ShapeType.Rectangle, + CornerRadius = 12, + BackgroundColor = Color.Parse("#66000000"), + Padding = new Thickness(10, 6), + Margin = new Thickness(0, 0, 0, 92), + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.End, + IsVisible = true, + Children = + { + new SkiaLabel() + { + Text = "PHOTO", + TextColor = Colors.White, + FontSize = 12, + UseCache = SkiaCacheType.Operations, + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center, + }.Assign(out LabelCaptureMode) + } + } + .ObserveBindingContext((me, vm, prop) => + { + bool attached = prop == nameof(BindingContext); + if (attached) + { + if (vm.CommandToggleCaptureMode != null) + me.OnTapped(shape => vm.CommandToggleCaptureMode.Execute(null)); + } + + if (attached || prop == nameof(vm.Mode) || prop == nameof(vm.IsRecording)) + { + // Hide while recording to avoid mid-record mode changes. + me.IsVisible = !vm.IsRecording; + LabelCaptureMode.Text = vm.Mode == CameraViewModel.CaptureUIMode.Video ? "VIDEO" : "PHOTO"; + } + }); + } + SkiaShape CreateControlsPanel() { return new SkiaShape() @@ -563,14 +663,39 @@ SkiaShape CreateCaptureButton() VerticalOptions = LayoutOptions.Fill } .Assign(out ButtonCapture) + .ObserveBindingContext((inner, vm, prop) => + { + bool attached = prop == nameof(BindingContext); + if (attached || prop == nameof(vm.Mode) || prop == nameof(vm.IsRecording)) + { + if (vm.IsRecording) + { + inner.Type = ShapeType.Rectangle; + inner.CornerRadius = 6; + inner.BackgroundColor = Colors.DarkRed; + } + else if (vm.Mode == CameraViewModel.CaptureUIMode.Video) + { + inner.Type = ShapeType.Rectangle; + inner.CornerRadius = 6; + inner.BackgroundColor = Color.Parse("#CECECE"); + } + else + { + inner.Type = ShapeType.Circle; + inner.BackgroundColor = Color.Parse("#CECECE"); + } + } + }) } } .ObserveBindingContext((me, vm, prop) => { bool attached = prop == nameof(BindingContext); - if (attached && vm.CommandCaptureStillPhoto != null) + if (attached) { - me.OnTapped(shape => vm.CommandCaptureStillPhoto.Execute(null)); + if (vm.CommandShutterTapped != null) + me.OnTapped(shape => vm.CommandShutterTapped.Execute(null)); } }); } @@ -801,6 +926,8 @@ void SyncUi() public void OpenHelp() { + return; + MainThread.BeginInvokeOnMainThread(() => { var popup = new HelpPopup(); diff --git a/src/app/Views/MainPageCameraFluent.cs b/src/app/Views/MainPageCameraFluent.cs index 23af2a5..b7f7fe3 100644 --- a/src/app/Views/MainPageCameraFluent.cs +++ b/src/app/Views/MainPageCameraFluent.cs @@ -329,8 +329,12 @@ private void OnContextPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(SkiaCamera.IsBusy)) { - var color = CameraControl.IsBusy ? Colors.DarkRed : Color.Parse("#CECECE"); - ButtonCapture.BackgroundColor = color; + if (_vm.IsRecording) + return; + + ButtonCapture.BackgroundColor = CameraControl.IsBusy + ? Colors.DarkRed + : Color.Parse("#CECECE"); } } @@ -423,7 +427,7 @@ void ApplyAspect() CameraControl.Aspect = TransformAspect.AspectFitFill; } - CameraControl.IsMirrored = IsMirrored; + CameraControl.MirrorPreviewX = IsMirrored; UserSettings.Current.Mirror = IsMirrored; UserSettings.Current.Fill = CameraControl.Aspect == TransformAspect.AspectCover;