diff --git a/.github/scripts/screenshots.ps1 b/.github/scripts/screenshots.ps1 index f155039..2d82dea 100644 --- a/.github/scripts/screenshots.ps1 +++ b/.github/scripts/screenshots.ps1 @@ -347,27 +347,29 @@ if ($ExpectedFlashes -gt 0) { # The file dialog takes a moment to go away; it must not be mistaken for the result. Wait-For { $d = Find-Dialog; $null -eq $d -or $d.Current.NativeWindowHandle -ne $fileHwnd } 20 "the file dialog to close" | Out-Null - # The stand-in phone has no odm: the app offers to create it in super first. - $create = Wait-For { Find-Dialog } 30 "the missing partition question" + # One summary before the flash: the phone, the file, and every point to know. Here the + # stand-in has no odm (created in super first) and the six physical partitions also go to + # slot b. A point that could cost data adds an "I have read this" box; tick it if it is there. + $summary = Wait-For { Find-Dialog } 30 "the flash summary" Start-Sleep -Milliseconds 600 - Save-Window ('{0:D2}-create-missing' -f $i); $i++ - Answer-Dialog $create 'Yes' - - $slots = Wait-For { Find-Dialog } 30 "the both-slots question" - Start-Sleep -Milliseconds 600 - Save-Window ('{0:D2}-both-slots' -f $i); $i++ - Answer-Dialog $slots 'Yes' - - # The app shows "operation completed" (or an error) when it is done. With the file dialog - # gone, the next window to appear is that box. A message box does not expose its text to - # UI Automation, so the fastboot log below is what proves the flash. - $done = Wait-For { Find-Dialog } 240 "flashing to finish" + Save-Window ('{0:D2}-flash-summary' -f $i); $i++ + $i++ # the both-slots question used to be its own screen; the number is kept so later links hold + $ackCond = New-Object System.Windows.Automation.PropertyCondition($UIA::ControlTypeProperty, $Type::CheckBox) + $ack = @($summary.FindAll($Scope::Descendants, $ackCond)) | Select-Object -First 1 + if ($null -ne $ack) { $ack.GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern).Toggle() } + Answer-Dialog $summary 'Flash' + + # The result is a strip on the Flash page (an error is still a box). The fastboot log below + # is what proves the flash. + $result = Wait-For { $d = Find-Dialog; if ($null -ne $d) { $d } else { Shown $root 'flash_result_title' } } 240 "flashing to finish" + if ($result.Current.ControlType -eq $Type::Window) { + $message = ($result.FindAll($Scope::Descendants, [System.Windows.Automation.Condition]::TrueCondition) | + ForEach-Object { $_.Current.Name } | Where-Object { $_ }) -join ' | ' + Fail "flashing ended with a box: $message" + } Start-Sleep -Milliseconds 800 - $message = ($done.FindAll($Scope::Descendants, [System.Windows.Automation.Condition]::TrueCondition) | - ForEach-Object { $_.Current.Name } | Where-Object { $_ }) -join ' | ' - Write-Host "dialog after flashing: $message" + Write-Host "after flashing: $((By-Id $root 'flash_result_title').Current.Name)" Save-Window ('{0:D2}-flash' -f $i); $i++ - Close-Dialog $done Remove-Item $fastbootRefuse -ErrorAction SilentlyContinue $flashes = @(Get-Content $fakeLog | Where-Object { $_ -match ' flash ' }).Count diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 525ceaf..3e505f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,9 +15,9 @@ jobs: name: core library and tests (linux) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: 8.0.x @@ -42,9 +42,9 @@ jobs: name: wpf app (windows) runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: 8.0.x @@ -114,7 +114,7 @@ jobs: Write-Host "Samsung's installer: $pos" if (-not ($pos -match 'samsung=True')) { throw "Samsung's own installer did not pass the signature check" } - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: FastbootStudio-win-x64 path: publish/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a533c1e..f30df25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,8 +21,9 @@ on: type: boolean default: false +# Only the release job writes to the repository (the release, the tag); the rest read. permissions: - contents: write + contents: read env: DOTNET_NOLOGO: true @@ -36,9 +37,9 @@ jobs: name: core tests (linux) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: 8.0.x @@ -50,7 +51,7 @@ jobs: needs: test runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Check the version shell: bash @@ -75,7 +76,7 @@ jobs: fi done - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: 8.0.x @@ -104,7 +105,7 @@ jobs: - name: Upload FastbootStudio.exe for signing id: unsigned if: steps.signpath.outputs.enabled == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: unsigned-exe path: publish/FastbootStudio.exe @@ -112,7 +113,7 @@ jobs: - name: Sign FastbootStudio.exe if: steps.signpath.outputs.enabled == 'true' - uses: signpath/github-action-submit-signing-request@v3 + uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} @@ -148,7 +149,7 @@ jobs: [System.IO.File]::WriteAllText("$zip.sha256", "$hash $zip`n") Get-Content "$zip.sha256" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: release-zip path: | @@ -160,10 +161,12 @@ jobs: name: publish and announce needs: build runs-on: ubuntu-latest + permissions: + contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: release-zip path: out @@ -217,7 +220,7 @@ jobs: if: env.PRE != 'true' run: bash .github/scripts/winget-manifests.sh "$RELEASE_VERSION" "$FILE" winget - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: env.PRE != 'true' with: name: winget-manifests @@ -236,7 +239,7 @@ jobs: - name: Submit to winget if: steps.winget.outputs.enabled == 'true' continue-on-error: true - uses: vedantmgoyal9/winget-releaser@v2 + uses: vedantmgoyal9/winget-releaser@b7370d4d152efdce803a26cc6ca9eee58cb2ad54 # v2 with: identifier: IFOKNR.FastbootStudio release-tag: ${{ env.RELEASE_VERSION }} diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index da45c3e..8f15520 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -27,11 +27,11 @@ jobs: capture: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 # full history so the commit step can rebase - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: dotnet-version: 8.0.x @@ -60,7 +60,7 @@ jobs: shell: pwsh run: ./.github/scripts/screenshots.ps1 -App publish/FastbootStudio.exe -Payload sample/sample-ota.zip -Out docs/screenshots -ExpectedFlashes 12 -ExpectedBackups 20 -Arabic - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() with: name: screenshots @@ -71,14 +71,16 @@ jobs: - name: Commit screenshots if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' shell: pwsh + env: + BRANCH: ${{ github.ref_name }} run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config user.name "IFOKNR" + git config user.email "253658289+ifoknr@users.noreply.github.com" git add docs/screenshots git diff --cached --quiet if ($LASTEXITCODE -ne 0) { git commit -m "Update app screenshots [skip ci]" # The branch may have moved while the app was being captured. - git pull --rebase origin ${{ github.ref_name }} - git push origin HEAD:${{ github.ref_name }} + git pull --rebase origin $env:BRANCH + git push origin HEAD:$env:BRANCH } diff --git a/.github/workflows/telegram.yml b/.github/workflows/telegram.yml index 17a6b28..bc14c6c 100644 --- a/.github/workflows/telegram.yml +++ b/.github/workflows/telegram.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Get the release env: diff --git a/AdbFacts.cs b/AdbFacts.cs index cbbc10f..77d5e35 100644 --- a/AdbFacts.cs +++ b/AdbFacts.cs @@ -56,6 +56,12 @@ public static void ReadConnected() } } + /// The Settings key: "on" when a connected phone may be asked for root on its own. + public const string RootProbeKey = "root_probe"; + + /// Whether a phone is asked for root when it connects; off until turned on in Settings. + public static bool RootProbe => Settings.Get(RootProbeKey) == "on"; + /// /// Asks the phone and stores what it said. Blocks, so call it on a worker thread. A phone /// that does not answer is simply not remembered. @@ -72,10 +78,12 @@ public static DeviceFacts Read(string serial) Settings.Set(FastbootUI.DeviceFactsKey, DeviceFacts.Remember(Settings.Get(FastbootUI.DeviceFactsKey), facts)); // The Companion lives under /data/adb, which only root can reach. Ask su only when - // the phone has one; Magisk shows a prompt the first time, KernelSU and APatch - // answer from their own allow list. - Adb.Result su = Adb.Run(AdbCommand.Shell(serial, "command -v su"), Adb.ShortCommand); - if (su.Succeeded && su.Output.Trim().Length > 0) + // the phone has one and the person turned the question on in Settings; Magisk + // shows a prompt the first time, KernelSU and APatch answer from their own allow list. + Adb.Result su = RootProbe + ? Adb.Run(AdbCommand.Shell(serial, "command -v su"), Adb.ShortCommand) + : null; + if (su != null && su.Succeeded && su.Output.Trim().Length > 0) { LogStore.Append("ADB device " + serial + ": asking for root to read Fastboot Studio Companion " + "(allow Shell in the root manager, or ignore this)"); diff --git a/Assets/icon-light.png b/Assets/icon-light.png new file mode 100644 index 0000000..d6bb419 Binary files /dev/null and b/Assets/icon-light.png differ diff --git a/Assets/icon-themed.png b/Assets/icon-themed.png index 1cd446e..519262a 100644 Binary files a/Assets/icon-themed.png and b/Assets/icon-themed.png differ diff --git a/BackupUI.cs b/BackupUI.cs index 3cebb4c..f79d5a5 100644 --- a/BackupUI.cs +++ b/BackupUI.cs @@ -42,6 +42,9 @@ static class BackupUI static string lastOutputFolder; + /// True while the poller sets the list's selection itself: the handler then stays out. + static bool choosing; + static volatile bool busy; static volatile bool pageVisible; static CancellationTokenSource cancel; @@ -79,6 +82,8 @@ public static void init() W.backup_devices.SelectionChanged += delegate { + if (choosing) + return; AdbDevice picked = W.backup_devices.SelectedItem as AdbDevice; if (picked != null && (current == null || picked.Serial != current.Serial)) selectDevice(picked); @@ -221,7 +226,9 @@ static void showDevices(List latest) return; } + choosing = true; W.backup_devices.SelectedItem = keep; + choosing = false; if (current == null || keep.Serial != current.Serial || keep.State != current.State) selectDevice(keep); } diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3feb7..8561061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ +## v3.0.0 +- شكل زجاجي جديد وأيقونة جديدة: رمادي زجاجي بالليلي، ثلجي بالنهاري +- القائمة بمجموعات، وشريط فوق يبيّن الجوال ووضعه، وشريط حالة تحت +- قبل الفلاش ملخص واحد فيه الجوال وكل النقاط، والنتيجة على الصفحة بدل نافذة +- سجل لكل فلاش ينحفظ كملف، وزر إيقاف للأمر المعلّق +- أودين: الحد من نوع الذاكرة، والترتيب BL ثم AP ثم CP ثم CSC +- اختصارات كيبورد، والصيني والياباني والكوري كاملين + +**English** +- A new glass look and icon: grey glass in dark, frosted in light +- Pages in groups, a bar above with the phone and its mode, a status bar below +- One summary before a flash with the phone and every point; the result on the page +- A log file per flash, and a Stop for a hung command +- Odin: limits from the storage type; BL, AP, CP, CSC order +- Keyboard shortcuts; Chinese, Japanese and Korean complete + ## v2.9.0 - جديد: فلاش سامسونج من التطبيق في وضع التحميل: صورة وحدة، tar من Magisk، أو الروم كامل - قبل ما يكتب: يفحص MD5 والـ lz4، وجدول PIT والموديل، ويرفض EFS وروم موديل ثاني diff --git a/DarkTitleBar.cs b/DarkTitleBar.cs index 6a596ad..61928a0 100644 --- a/DarkTitleBar.cs +++ b/DarkTitleBar.cs @@ -18,6 +18,23 @@ static class DarkTitleBar const int DWMWA_BORDER_COLOR = 34; const int DWMWA_CAPTION_COLOR = 35; const int DWMWA_TEXT_COLOR = 36; + const int DWMWA_SYSTEMBACKDROP_TYPE = 38; + const int DWMSBT_NONE = 1; + const int DWMSBT_MAINWINDOW = 2; // Mica + + /// The Settings key of the glass backdrop: "off" to keep the window's own gradient. + public const string BackdropKey = "backdrop"; + + /// Windows 11 22H2 and later draw Mica; older systems have nothing to turn on. + public static bool BackdropAvailable => Environment.OSVersion.Version.Build >= 22621; + + /// + /// Off unless turned on in Settings, and only with graphics-card drawing: under software + /// drawing (the app's default) Windows paints nothing behind a clear window, so the page + /// turned black. + /// + public static bool BackdropWanted => BackdropAvailable && Rendering.Chosen == Rendering.Mode.Gpu + && Settings.Get(BackdropKey) == "on"; /// COLORREF (0x00BBGGRR) of one of the theme's brushes: Bg, Line or Text. static int ColorRef(string key) @@ -49,6 +66,22 @@ public static void Apply(Window window) DwmSetWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, ref caption, sizeof(int)); DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, ref border, sizeof(int)); DwmSetWindowAttribute(hwnd, DWMWA_TEXT_COLOR, ref text, sizeof(int)); + + // Mica: the desktop shows through the window, blurred. The window's own + // background has to be clear for it to show, so the sky brush goes when it is on. + if (BackdropAvailable) + { + bool mica = BackdropWanted; + int backdrop = mica ? DWMSBT_MAINWINDOW : DWMSBT_NONE; + if (DwmSetWindowAttribute(hwnd, DWMWA_SYSTEMBACKDROP_TYPE, ref backdrop, sizeof(int)) == 0) + { + HwndSource source = HwndSource.FromHwnd(hwnd); + if (source?.CompositionTarget != null) + source.CompositionTarget.BackgroundColor = mica ? System.Windows.Media.Colors.Transparent + : ((System.Windows.Media.SolidColorBrush)Palette.Get("Bg")).Color; + window.Background = mica ? System.Windows.Media.Brushes.Transparent : Palette.Get("SkyBrush"); + } + } } catch (DllNotFoundException) { diff --git a/DriverWindow.xaml b/DriverWindow.xaml index 0034608..ec984da 100644 --- a/DriverWindow.xaml +++ b/DriverWindow.xaml @@ -6,8 +6,8 @@ SizeToContent="Height" Width="596" ShowInTaskbar="False" WindowStartupLocation="CenterOwner" Foreground="{StaticResource Text}" FontFamily="{StaticResource UiFont}" FontSize="13" UseLayoutRounding="True"> - + @@ -18,7 +18,7 @@ - + + BorderBrush="{StaticResource Line}" BorderThickness="0,1,0,0" CornerRadius="0,0,15,15"> + - - - + + + void addButtons(MessageBoxResult requestedDefault, MessageBoxResult fallbackDefault, - MessageBoxResult cancelResult, bool risky, params MessageBoxResult[] order) + MessageBoxResult cancelResult, bool risky, string mainLabel, params MessageBoxResult[] order) { MessageBoxResult chosenDefault = order.Contains(requestedDefault) ? requestedDefault : fallbackDefault; @@ -125,7 +159,7 @@ void addButtons(MessageBoxResult requestedDefault, MessageBoxResult fallbackDefa Button button = new Button { - Content = label(value), + Content = main && mainLabel != null ? mainLabel : label(value), MinWidth = 96, Margin = new Thickness(i == 0 ? 0 : 10, 0, 0, 0), IsDefault = value == chosenDefault, @@ -143,6 +177,48 @@ void addButtons(MessageBoxResult requestedDefault, MessageBoxResult fallbackDefa } } + /// One point of a summary: a small glyph (amber for a destructive one) and the text. + UIElement summaryRow(SummaryItem point) + { + string color = point.Destructive ? "Warn" : "Accent"; + Border row = new Border + { + Margin = new Thickness(0, 0, 0, 8), + Padding = new Thickness(12, 9, 12, 9), + CornerRadius = new CornerRadius(10), + BorderThickness = new Thickness(1), + Background = Palette.Get(point.Destructive ? "WarnSoft" : "Raised"), + BorderBrush = Palette.Get(point.Destructive ? "WarnLine" : "Line"), + }; + DockPanel panel = new DockPanel(); + System.Windows.Shapes.Path mark = new System.Windows.Shapes.Path + { + Width = 15, + Height = 15, + Stretch = Stretch.Uniform, + StrokeThickness = 2, + StrokeStartLineCap = PenLineCap.Round, + StrokeEndLineCap = PenLineCap.Round, + StrokeLineJoin = PenLineJoin.Round, + Stroke = Palette.Get(color), + Data = Geometry.Parse(point.Destructive ? GlyphWarning : GlyphInfo), + VerticalAlignment = VerticalAlignment.Top, + Margin = new Thickness(0, 2, 10, 0), + FlowDirection = FlowDirection.LeftToRight, + }; + DockPanel.SetDock(mark, Dock.Left); + panel.Children.Add(mark); + panel.Children.Add(new TextBlock + { + Text = point.Text, + TextWrapping = TextWrapping.Wrap, + FontSize = 12.5, + LineHeight = 18, + }); + row.Child = panel; + return row; + } + void onKey(object sender, KeyEventArgs e) { // Ctrl+C copies the message, as a Windows message box does; handy for error reports. @@ -230,8 +306,21 @@ public static void Done(string text) show(text, null, MessageBoxButton.OK, Kind.Done, MessageBoxResult.None); } + /// + /// One question before a flash instead of several: what is written and to which phone + /// (), then every point worth knowing. When a point can cost + /// data, the main button waits for the "I have read this" box. True to go ahead. + /// + public static bool Summary(string title, string text, string deviceLine, IList points, string goLabel) + { + bool destructive = points != null && points.Any(p => p.Destructive); + return show(text, title, MessageBoxButton.OKCancel, destructive ? Kind.Warning : Kind.Question, + MessageBoxResult.Cancel, null, goLabel, deviceLine, points) == MessageBoxResult.OK; + } + static MessageBoxResult show(string text, string title, MessageBoxButton choice, Kind kind, - MessageBoxResult defaultResult, string requiredWord = null) + MessageBoxResult defaultResult, string requiredWord = null, string mainLabel = null, string deviceLine = null, + IList points = null) { Application app = Application.Current; if (app == null) @@ -239,9 +328,9 @@ static MessageBoxResult show(string text, string title, MessageBoxButton choice, // Like MessageBox, callable from any thread; the window itself lives on the UI thread. if (!app.Dispatcher.CheckAccess()) - return app.Dispatcher.Invoke(() => show(text, title, choice, kind, defaultResult, requiredWord)); + return app.Dispatcher.Invoke(() => show(text, title, choice, kind, defaultResult, requiredWord, mainLabel, deviceLine, points)); - ThemedDialog dialog = new ThemedDialog(text, title, choice, kind, defaultResult, requiredWord); + ThemedDialog dialog = new ThemedDialog(text, title, choice, kind, defaultResult, requiredWord, mainLabel, deviceLine, points); Window owner = app.Windows.OfType() .FirstOrDefault(window => window != dialog && window.IsActive && window.IsVisible) ?? (MainWindow.THIS != null && MainWindow.THIS.IsVisible ? MainWindow.THIS : null); diff --git a/Themes/Palette.Dark.xaml b/Themes/Palette.Dark.xaml index 2560799..549c701 100644 --- a/Themes/Palette.Dark.xaml +++ b/Themes/Palette.Dark.xaml @@ -2,42 +2,61 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:props="clr-namespace:FastbootEnhance.Properties"> - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + diff --git a/Themes/Palette.Light.xaml b/Themes/Palette.Light.xaml index dc68e79..8e350df 100644 --- a/Themes/Palette.Light.xaml +++ b/Themes/Palette.Light.xaml @@ -2,39 +2,54 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:props="clr-namespace:FastbootEnhance.Properties"> - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + diff --git a/Themes/Styles.xaml b/Themes/Styles.xaml index 224a267..fae0ca1 100644 --- a/Themes/Styles.xaml +++ b/Themes/Styles.xaml @@ -1,6 +1,7 @@ + xmlns:props="clr-namespace:FastbootEnhance.Properties" + xmlns:local="clr-namespace:FastbootEnhance"> @@ -81,11 +82,13 @@ + @@ -103,29 +106,30 @@ + + + @@ -198,9 +230,9 @@ - + - + @@ -234,7 +266,7 @@ - @@ -254,8 +286,8 @@ - + + + + + + + + + + + + + + + + + + + + + + + +