Repository: lswiderski/mi-scale-exporter
Branch: main
Commit: 1063bcb5e511
Files: 106
Total size: 308.8 KB
Directory structure:
gitextract_nz2lm8kb/
├── .gitignore
├── .idea/
│ ├── .idea.MiScaleExporter/
│ │ └── .idea/
│ │ ├── .name
│ │ ├── encodings.xml
│ │ ├── indexLayout.xml
│ │ ├── misc.xml
│ │ ├── projectSettingsUpdater.xml
│ │ └── vcs.xml
│ └── .idea.miscale2garmin/
│ └── .idea/
│ ├── .gitignore
│ ├── .name
│ ├── encodings.xml
│ ├── indexLayout.xml
│ ├── misc.xml
│ └── vcs.xml
├── LICENSE
├── MiScaleExporter.sln
├── README.md
├── docs/
│ ├── _config.yml
│ ├── _includes/
│ │ ├── head-custom-google-analytics.html
│ │ └── head-custom.html
│ ├── _layouts/
│ │ └── default.html
│ ├── _sass/
│ │ ├── cayman.scss
│ │ ├── jekyll-theme-cayman.scss
│ │ ├── normalize.scss
│ │ ├── rouge-github.scss
│ │ └── variables.scss
│ └── index.md
└── src/
└── MiScaleExporter.MAUI/
├── App.xaml
├── App.xaml.cs
├── AppShell.xaml
├── AppShell.xaml.cs
├── Behaviors/
│ ├── NumericDoubleValidationBehavior.cs
│ └── NumericIntValidationBehavior.cs
├── Controls/
│ ├── FlyoutFooter.xaml
│ ├── FlyoutFooter.xaml.cs
│ ├── FlyoutHeader.xaml
│ └── FlyoutHeader.xaml.cs
├── Converters/
│ └── InvertedBoolConverter.cs
├── MauiProgram.cs
├── MiScaleExporter.MAUI.csproj
├── Models/
│ ├── BodyComposition.cs
│ ├── GarminApiResponse.cs
│ ├── GarminBodyCompositionRequest.cs
│ ├── GarminExternalApiResponse.cs
│ ├── GarminFitFileCreationResult.cs
│ ├── GarminUploadResult.cs
│ ├── PreferencesKeys.cs
│ ├── ScaleMeasurement.cs
│ ├── ScaleType.cs
│ ├── SettingKeys.cs
│ ├── Sex.cs
│ └── User.cs
├── Permission/
│ ├── BluetoothConnectPermission.cs
│ └── IBluetoothConnectPermission.cs
├── Platforms/
│ ├── Android/
│ │ ├── AndroidManifest.xml
│ │ ├── MainActivity.cs
│ │ ├── MainApplication.cs
│ │ └── Resources/
│ │ └── values/
│ │ ├── colors.xml
│ │ └── styles.xml
│ ├── MacCatalyst/
│ │ ├── AppDelegate.cs
│ │ ├── Info.plist
│ │ └── Program.cs
│ ├── Tizen/
│ │ ├── Main.cs
│ │ └── tizen-manifest.xml
│ ├── Windows/
│ │ ├── App.xaml
│ │ ├── App.xaml.cs
│ │ ├── Package.appxmanifest
│ │ └── app.manifest
│ └── iOS/
│ ├── AppDelegate.cs
│ ├── Info.plist
│ └── Program.cs
├── Properties/
│ └── launchSettings.json
├── Resources/
│ ├── Fonts/
│ │ ├── FontAwesome6Regular.otf
│ │ └── FontAwesome6Solid.otf
│ ├── Localization/
│ │ ├── AppSnippets.Designer.cs
│ │ ├── AppSnippets.pl.resx
│ │ └── AppSnippets.resx
│ ├── Raw/
│ │ └── AboutAssets.txt
│ └── Styles/
│ ├── Colors.xaml
│ └── Styles.xaml
├── Services/
│ ├── DataInterpreter.cs
│ ├── GarminService.cs
│ ├── IDataInterpreter.cs
│ ├── IGarminService.cs
│ ├── ILogService.cs
│ ├── IScale.cs
│ ├── LogService.cs
│ └── Scale.cs
├── Utils/
│ └── DoubleValueParser.cs
├── ViewModels/
│ ├── AboutViewModel.cs
│ ├── BaseViewModel.cs
│ ├── FormViewModel.cs
│ ├── IFormViewModel.cs
│ ├── IScaleViewModel.cs
│ ├── ISettingsViewModel.cs
│ ├── ScaleViewModel.cs
│ └── SettingsViewModel.cs
└── Views/
├── AboutPage.xaml
├── AboutPage.xaml.cs
├── FormPage.xaml
├── FormPage.xaml.cs
├── HelpPage.xaml
├── HelpPage.xaml.cs
├── ScalePage.xaml
├── ScalePage.xaml.cs
├── SettingsPage.xaml
└── SettingsPage.xaml.cs
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Created by https://www.toptal.com/developers/gitignore/api/rider,visualstudio,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=rider,visualstudio,visualstudiocode
### Rider ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# Support for Project snippet scope
### VisualStudio ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
*.code-workspace
# Local History for Visual Studio Code
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
### VisualStudio Patch ###
# Additional files built by Visual Studio
# End of https://www.toptal.com/developers/gitignore/api/rider,visualstudio,visualstudiocode
================================================
FILE: .idea/.idea.MiScaleExporter/.idea/.name
================================================
MiScaleExporter
================================================
FILE: .idea/.idea.MiScaleExporter/.idea/encodings.xml
================================================
================================================
FILE: .idea/.idea.MiScaleExporter/.idea/indexLayout.xml
================================================
================================================
FILE: .idea/.idea.MiScaleExporter/.idea/misc.xml
================================================
================================================
FILE: .idea/.idea.MiScaleExporter/.idea/projectSettingsUpdater.xml
================================================
================================================
FILE: .idea/.idea.MiScaleExporter/.idea/vcs.xml
================================================
================================================
FILE: .idea/.idea.miscale2garmin/.idea/.gitignore
================================================
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/contentModel.xml
/.idea.miscale2garmin.iml
/.idea.MiScaleExporter.iml
/projectSettingsUpdater.xml
/modules.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
================================================
FILE: .idea/.idea.miscale2garmin/.idea/.name
================================================
miscale2garmin
================================================
FILE: .idea/.idea.miscale2garmin/.idea/encodings.xml
================================================
================================================
FILE: .idea/.idea.miscale2garmin/.idea/indexLayout.xml
================================================
================================================
FILE: .idea/.idea.miscale2garmin/.idea/misc.xml
================================================
================================================
FILE: .idea/.idea.miscale2garmin/.idea/vcs.xml
================================================
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: MiScaleExporter.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiScaleExporter.MAUI", "src\MiScaleExporter.MAUI\MiScaleExporter.MAUI.csproj", "{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|iPhone = Debug|iPhone
Debug|iPhoneSimulator = Debug|iPhoneSimulator
Release|Any CPU = Release|Any CPU
Release|iPhone = Release|iPhone
Release|iPhoneSimulator = Release|iPhoneSimulator
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Debug|iPhone.Build.0 = Debug|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Debug|iPhone.Deploy.0 = Debug|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Release|Any CPU.Build.0 = Release|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Release|Any CPU.Deploy.0 = Release|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Release|iPhone.ActiveCfg = Release|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Release|iPhone.Build.0 = Release|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Release|iPhone.Deploy.0 = Release|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{39C1BB45-DB3C-4C1A-A3C0-5CBB35C753D0}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {44FCA853-2BA4-4001-945E-910A5CBFFEB1}
EndGlobalSection
EndGlobal
================================================
FILE: README.md
================================================
# mi-scale-exporter
Mobile App to export data from Mi Body Composition Scale (works with Mi Scale too) and upload it to Garmin Connect Cloud. It also allows you to upload manually entered body composition data to the Garmin cloud.
Instruction: [https://lswiderski.github.io/mi-scale-exporter/](https://lswiderski.github.io/mi-scale-exporter/)
Tested on Oneplus 5T (Android 10) and Mi Body Composition Scale (XMTZC02HM)
## Download
- Google play: https://play.google.com/store/apps/details?id=com.lukaszswiderski.MiScaleExporter
- APK/AAB installers: https://github.com/lswiderski/mi-scale-exporter/releases
## iOS/iPadOS version
Check out this web project: https://github.com/lswiderski/WebBodyComposition
## Instruction
- Stand on your scale. Measure yourself. Complete the user form data, Scale Bluetooth address and get data from the scale. Mi Body Composition Scale is active up to 15 min after the measurement. (Bluetooth address can be found in Zepp Life > Profile > My devices > Mi Body Composition Scale > Bluetooth address (hold to copy)).
- If your scale supports "Weigh small object" - turn it off
- Then you can review your data and upload it to Garmin Cloud. If you do not have Mi scale and just want to manually insert the data, you can so.
- You can save the Garmin password in this App but you don't have to. Passwords Managers like KeePass2 works well too. If you do not provide a password in the settings, you will be asked for it each time before sending. No password will be saved in app.
- This app support 2FA/MFA codes (since v2.1.0)
- This App pass your data, email and password directly to Garmin Connect Cloud or you can change it to proxy API server and then it sends to Garmin Cloud.
- The Proxy API does not store or log anything, it's just a middleware between this App and Garmin services.
- Proxy API repository: https://github.com/lswiderski/yet-another-garmin-connect-client
- If you afraid of your data, you can host your own API server. Just change the server address in Settings. For now you can use default one: https://frog01-20364.wykr.es
## Diagram of the flow with Web Proxy
```mermaid
sequenceDiagram
participant Mobile App
participant Mi Body Composition Scale
participant API Endpoint Proxy
participant Garmin Cloud
Mobile App->>Mi Body Composition Scale: Connect and get data
Mi Body Composition Scale-->>Mobile App: Weight and Impedance data
loop
Mobile App->>Mobile App: Calculate Body Composition
end
Mobile App->>API Endpoint Proxy: Body Composition data
API Endpoint Proxy->>Garmin Cloud: Body Composition data
Garmin Cloud-->>API Endpoint Proxy: Result
API Endpoint Proxy-->>Mobile App: Result
```
## API Endpoint used in the app ([source](https://github.com/lswiderski/yet-another-garmin-connect-client))
```http
https://frog01-20364.wykr.es
```
## Diagram of the flow with direct send to Garmin Cloud
```mermaid
sequenceDiagram
participant Mobile App
participant Mi Body Composition Scale
participant Garmin Cloud
Mobile App->>Mi Body Composition Scale: Connect and get data
Mi Body Composition Scale-->>Mobile App: Weight and Impedance data
loop
Mobile App->>Mobile App: Calculate Body Composition
end
Mobile App->>Garmin Cloud: Body Composition data
Garmin Cloud-->>Mobile App: Result
```
## Stack
- MAUI & .NET 7 (C#)
- Autofac
- Plugin.BLE - To receive data via Bluetooth from Mi scale
- Xamarin.Essentials
- API Backend in C# (YAGCC project)
## Images
- Xiaomi settings (Bluetooth adress - Zepp Life)

- required user data

- settings

- measure

- Calculated body composition

- results in Garmin Cloud

## Inspiration
- https://github.com/RobertWojtowicz/miscale2garmin
- https://github.com/davidkroell/bodycomposition
## If you like my work, you can buy me a coffee
================================================
FILE: docs/_config.yml
================================================
title: MiScale Exporter
description: Mobile App to export data from Mi Body Composition Scale and upload it to Garmin Connect Cloud
show_downloads: true
google_analytics:
theme: jekyll-theme-cayman
================================================
FILE: docs/_includes/head-custom-google-analytics.html
================================================
{% if site.google_analytics %}
{% endif %}
================================================
FILE: docs/_includes/head-custom.html
================================================
{% include head-custom-google-analytics.html %}
================================================
FILE: docs/_layouts/default.html
================================================
{% if site.github.is_project_page %}
View on GitHub
{% endif %}
{% if site.show_downloads %}
AndroidWeb version (iOS)
{% endif %}
{{ content }}
================================================
FILE: docs/_sass/cayman.scss
================================================
// Placeholder file. If your site uses
// @import "{{ site.theme }}";
// Then using this theme with jekyll-remote-theme will work fine.
@import "jekyll-theme-cayman";
================================================
FILE: docs/_sass/jekyll-theme-cayman.scss
================================================
@import "normalize";
@import "rouge-github";
@import "variables";
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700&display=swap');
@mixin large {
@media screen and (min-width: #{$large-breakpoint}) {
@content;
}
}
@mixin medium {
@media screen and (min-width: #{$medium-breakpoint}) and (max-width: #{$large-breakpoint}) {
@content;
}
}
@mixin small {
@media screen and (max-width: #{$medium-breakpoint}) {
@content;
}
}
* {
box-sizing: border-box;
}
body {
padding: 0;
margin: 0;
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.5;
color: $body-text-color;
}
#skip-to-content {
height: 1px;
width: 1px;
position: absolute;
overflow: hidden;
top: -10px;
&:focus {
position: fixed;
top: 10px;
left: 10px;
height: auto;
width: auto;
background: invert($body-link-color);
outline: thick solid invert($body-link-color);
}
}
a {
color: $body-link-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.btn {
display: inline-block;
margin-bottom: 1rem;
color: rgba(255, 255, 255, 0.7);
background-color: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
border-style: solid;
border-width: 1px;
border-radius: 0.3rem;
transition: color 0.2s, background-color 0.2s, border-color 0.2s;
&:hover {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
}
+ .btn {
margin-left: 1rem;
}
@include large {
padding: 0.75rem 1rem;
}
@include medium {
padding: 0.6rem 0.9rem;
font-size: 0.9rem;
}
@include small {
display: block;
width: 100%;
padding: 0.75rem;
font-size: 0.9rem;
+ .btn {
margin-top: 1rem;
margin-left: 0;
}
}
}
.page-header {
color: $header-heading-color;
text-align: center;
background-color: $header-bg-color;
background-image: linear-gradient(120deg, $header-bg-color-secondary, $header-bg-color);
@include large {
padding: 5rem 6rem;
}
@include medium {
padding: 3rem 4rem;
}
@include small {
padding: 2rem 1rem;
}
}
.project-name {
margin-top: 0;
margin-bottom: 0.1rem;
@include large {
font-size: 3.25rem;
}
@include medium {
font-size: 2.25rem;
}
@include small {
font-size: 1.75rem;
}
}
.project-tagline {
margin-bottom: 2rem;
font-weight: normal;
opacity: 0.7;
@include large {
font-size: 1.25rem;
}
@include medium {
font-size: 1.15rem;
}
@include small {
font-size: 1rem;
}
}
.main-content {
word-wrap: break-word;
:first-child {
margin-top: 0;
}
@include large {
max-width: 64rem;
padding: 2rem 6rem;
margin: 0 auto;
font-size: 1.1rem;
}
@include medium {
padding: 2rem 4rem;
font-size: 1.1rem;
}
@include small {
padding: 2rem 1rem;
font-size: 1rem;
}
kbd {
background-color: #fafbfc;
border: 1px solid #c6cbd1;
border-bottom-color: #959da5;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #959da5;
color: #444d56;
display: inline-block;
font-size: 11px;
line-height: 10px;
padding: 3px 5px;
vertical-align: middle;
}
img {
max-width: 100%;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 2rem;
margin-bottom: 1rem;
font-weight: normal;
color: $section-headings-color;
}
p {
margin-bottom: 1em;
}
code {
padding: 2px 4px;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 0.9rem;
color: $code-text-color;
background-color: $code-bg-color;
border-radius: 0.3rem;
}
pre {
padding: 0.8rem;
margin-top: 0;
margin-bottom: 1rem;
font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace;
color: $code-text-color;
word-wrap: normal;
background-color: $code-bg-color;
border: solid 1px $border-color;
border-radius: 0.3rem;
> code {
padding: 0;
margin: 0;
font-size: 0.9rem;
color: $code-text-color;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
}
.highlight {
margin-bottom: 1rem;
pre {
margin-bottom: 0;
word-break: normal;
}
}
.highlight pre,
pre {
padding: 0.8rem;
overflow: auto;
font-size: 0.9rem;
line-height: 1.45;
border-radius: 0.3rem;
-webkit-overflow-scrolling: touch;
}
pre code,
pre tt {
display: inline;
max-width: initial;
padding: 0;
margin: 0;
overflow: initial;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
&:before,
&:after {
content: normal;
}
}
ul,
ol {
margin-top: 0;
}
blockquote {
padding: 0 1rem;
margin-left: 0;
color: $blockquote-text-color;
border-left: 0.3rem solid $border-color;
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
}
table {
display: block;
width: 100%;
overflow: auto;
word-break: normal;
word-break: keep-all; // For Firefox to horizontally scroll wider tables.
-webkit-overflow-scrolling: touch;
th {
font-weight: bold;
}
th,
td {
padding: 0.5rem 1rem;
border: 1px solid $table-border-color;
}
}
dl {
padding: 0;
dt {
padding: 0;
margin-top: 1rem;
font-size: 1rem;
font-weight: bold;
}
dd {
padding: 0;
margin-bottom: 1rem;
}
}
hr {
height: 2px;
padding: 0;
margin: 1rem 0;
background-color: $hr-border-color;
border: 0;
}
}
.site-footer {
padding-top: 2rem;
margin-top: 2rem;
border-top: solid 1px $hr-border-color;
@include large {
font-size: 1rem;
}
@include medium {
font-size: 1rem;
}
@include small {
font-size: 0.9rem;
}
}
.site-footer-owner {
display: block;
font-weight: bold;
}
.site-footer-credits {
color: $blockquote-text-color;
}
================================================
FILE: docs/_sass/normalize.scss
================================================
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */ /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}
================================================
FILE: docs/_sass/rouge-github.scss
================================================
.highlight table td { padding: 5px; }
.highlight table pre { margin: 0; }
.highlight .cm {
color: #999988;
font-style: italic;
}
.highlight .cp {
color: #999999;
font-weight: bold;
}
.highlight .c1 {
color: #999988;
font-style: italic;
}
.highlight .cs {
color: #999999;
font-weight: bold;
font-style: italic;
}
.highlight .c, .highlight .cd {
color: #999988;
font-style: italic;
}
.highlight .err {
color: #a61717;
background-color: #e3d2d2;
}
.highlight .gd {
color: #000000;
background-color: #ffdddd;
}
.highlight .ge {
color: #000000;
font-style: italic;
}
.highlight .gr {
color: #aa0000;
}
.highlight .gh {
color: #999999;
}
.highlight .gi {
color: #000000;
background-color: #ddffdd;
}
.highlight .go {
color: #888888;
}
.highlight .gp {
color: #555555;
}
.highlight .gs {
font-weight: bold;
}
.highlight .gu {
color: #aaaaaa;
}
.highlight .gt {
color: #aa0000;
}
.highlight .kc {
color: #000000;
font-weight: bold;
}
.highlight .kd {
color: #000000;
font-weight: bold;
}
.highlight .kn {
color: #000000;
font-weight: bold;
}
.highlight .kp {
color: #000000;
font-weight: bold;
}
.highlight .kr {
color: #000000;
font-weight: bold;
}
.highlight .kt {
color: #445588;
font-weight: bold;
}
.highlight .k, .highlight .kv {
color: #000000;
font-weight: bold;
}
.highlight .mf {
color: #009999;
}
.highlight .mh {
color: #009999;
}
.highlight .il {
color: #009999;
}
.highlight .mi {
color: #009999;
}
.highlight .mo {
color: #009999;
}
.highlight .m, .highlight .mb, .highlight .mx {
color: #009999;
}
.highlight .sb {
color: #d14;
}
.highlight .sc {
color: #d14;
}
.highlight .sd {
color: #d14;
}
.highlight .s2 {
color: #d14;
}
.highlight .se {
color: #d14;
}
.highlight .sh {
color: #d14;
}
.highlight .si {
color: #d14;
}
.highlight .sx {
color: #d14;
}
.highlight .sr {
color: #009926;
}
.highlight .s1 {
color: #d14;
}
.highlight .ss {
color: #990073;
}
.highlight .s {
color: #d14;
}
.highlight .na {
color: #008080;
}
.highlight .bp {
color: #999999;
}
.highlight .nb {
color: #0086B3;
}
.highlight .nc {
color: #445588;
font-weight: bold;
}
.highlight .no {
color: #008080;
}
.highlight .nd {
color: #3c5d5d;
font-weight: bold;
}
.highlight .ni {
color: #800080;
}
.highlight .ne {
color: #990000;
font-weight: bold;
}
.highlight .nf {
color: #990000;
font-weight: bold;
}
.highlight .nl {
color: #990000;
font-weight: bold;
}
.highlight .nn {
color: #555555;
}
.highlight .nt {
color: #000080;
}
.highlight .vc {
color: #008080;
}
.highlight .vg {
color: #008080;
}
.highlight .vi {
color: #008080;
}
.highlight .nv {
color: #008080;
}
.highlight .ow {
color: #000000;
font-weight: bold;
}
.highlight .o {
color: #000000;
font-weight: bold;
}
.highlight .w {
color: #bbbbbb;
}
.highlight {
background-color: #f8f8f8;
}
================================================
FILE: docs/_sass/variables.scss
================================================
// Breakpoints
$large-breakpoint: 64em !default;
$medium-breakpoint: 42em !default;
// Headers
$header-heading-color: #fff !default;
$header-bg-color: #159957 !default;
$header-bg-color-secondary: #155799 !default;
// Text
$section-headings-color: #159957 !default;
$body-text-color: #606c71 !default;
$body-link-color: #1e6bb8 !default;
$blockquote-text-color: #819198 !default;
// Code
$code-bg-color: #f3f6fa !default;
$code-text-color: #567482 !default;
// Borders
$border-color: #dce6f0 !default;
$table-border-color: #e9ebec !default;
$hr-border-color: #eff0f1 !default;
================================================
FILE: docs/index.md
================================================
---
layout: default
---
Mobile App to export data from Xiaomi Scales:
- Mi Smart Scale
- [Mi Body Composition Scale 1 and 2](#steps-to-connect-mi-body-compostion-scale-1-and-2)
- [Mi Body Composition Scale S400](#steps-to-connect-xiaomi-body-composition-scale-s400) (only Android)
and upload it to [Garmin Connect Cloud.](#garmin-connect-upload)
It also allows you to upload manually entered body composition data to the Garmin cloud.
> [!CAUTION]
> This application is not supported or endorsed by Xiaomi or Garmin. So it could stop working at any moment. It is intended for personal use only and is not to be used for financial gain. The creator takes no responsibility for any consequences that may arise from its use.
## If you like my work, you can buy me a coffee
## Steps to Connect Mi Body Compostion Scale 1 and 2:
1. Add Scale to Zepp Life app.
2. You will need the Bluetooth address of the scale. Go to Zepp Life > Profile > My devices > Mi Body Composition Scale > Bluetooth address (hold to copy)
3. If your scale supports "Weigh small object" - turn it off
4. Open Settings in MiSCale Exporter. Select Scale Model to Mi Body Compositon Scale 1 / 2 and paste Bluetooth address.
5. Now it's time for measurement. Stand on your scale. Measure yourself and get data from the scale. Mi Body Composition Scale is active up to 15 min after the measurement.
6. These types of scales do not measure body composition. They measure weight and impedance and estimate the result based on those measurements. The exact calculation algorithm is unknown, but with the help of reverse engineers, an approximate one has been achieved, which gives a satisfactory result. However, this means that the final result may differ slightly from that provided by the application. For this calculation proper age, height and sex is needed.
## Steps to Connect Xiaomi Body Composition Scale S400:
1. Add Scale to Xiaomi Home App and do first measurement. You can disable Heart rate measurement to fast up whole process.
2. You will need scale MAC address and BLE Key from Xiaomi Cloud. You can get it on many ways but I recommend 'Xiaomi Cloud Tokens Extractor'
Go to https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor and use your preferred way.
Find Xiaomi Body Composition Scale S400 on the list of your devices and copy BLE KEY and MAC and save it for later.
3. Now you need to completely kill the app (so that it doesn't run in the background either). Or remove the scale from the list of devices - if you use other devices with Xiaomi Home. The scale will only send the needed data when it is not able to connect to the Xiaomi Home app! Every time when you add Scale as new device to Xiaomi Home new BLE key will be generated.
4. Now got MiScale Exporter settings, select S400 scale and paste MAC address and BLE Key.
5. The scale only sends data in a short time window at the end of weighing, so it's important to start the measurement before stepping on the scale. The Bluetooth icon should blink. (Watch the video below)
[](https://www.youtube.com/shorts/HtOZZwnkZHw)
6. These types of scales do not measure body composition. They measure weight and impedance and estimate the result based on those measurements. The scale sends 3 values: Weight, impedance and Heart rate. To receive body composition data, impedance is processed by an algorithm known to be similar to that used by the Mi Body Composition Scale 2 (different from that used by the S400). Because of this, the result may differ from that of the Xiaomi Home app. For this calculation proper age, height and sex is needed.
7. Bear in mind that it is an experimental solution and errors may occur. If you encounter them, please contact me.
## Garmin Connect Upload
1. After successfully retrieving data from the scale, you will be redirected to the Garmin data form.
2. Here you can see the result of your measurement and send it to the Garmin Connect cloud.
3. If you have set up your email and password in the settings, you do not need to enter anything else here.
4. This app support 2FA/MFA codes. If you use MFA security, you will receive a message asking you to enter a code when you first try to send a message. It should be sent to your email address or as a text message to your phone (depending on your region). Close the message and enter the code in the new MFA field that appears at the bottom of the screen.
5. You don't have to enter the code every time you try to send a message. The returned token will be saved in the app and will be valid for several months.
================================================
FILE: src/MiScaleExporter.MAUI/App.xaml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/App.xaml.cs
================================================
using Autofac;
using Autofac.Extras.CommonServiceLocator;
using CommonServiceLocator;
using MiScaleExporter.Models;
using MiScaleExporter.Services;
using MiScaleExporter.MAUI.ViewModels;
using IContainer = Autofac.IContainer;
using MiScaleExporter.Droid;
using System.Globalization;
using CommunityToolkit.Maui.Storage;
namespace MiScaleExporter.MAUI
{
public partial class App : Application
{
public static IContainer Container;
public static BodyComposition BodyComposition;
public App()
{
CultureInfo.DefaultThreadCurrentCulture = Thread.CurrentThread.CurrentCulture;
InitializeComponent();
}
protected override Window CreateWindow(IActivationState activationState)
{
// Workaround for: 'Either set MainPage or override CreateWindow.'??
if (this.MainPage == null)
{
AutofacInit();
this.MainPage = new AppShell();
}
return base.CreateWindow(activationState);
}
protected override void OnStart()
{
base.OnStart();
AutofacInit();
MainPage = new AppShell();
}
protected void AutofacInit()
{
// Initialize Autofac builder
var builder = new ContainerBuilder();
// Register services
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().AsSelf();
builder.RegisterInstance(FileSaver.Default).SingleInstance();
App.Container = builder.Build();
ServiceLocator.SetLocatorProvider(() => new AutofacServiceLocator(Container));
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/AppShell.xaml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/AppShell.xaml.cs
================================================
namespace MiScaleExporter.MAUI
{
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
private async void OnMenuItemClicked(object sender, EventArgs e)
{
await Shell.Current.GoToAsync("..");
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Behaviors/NumericDoubleValidationBehavior.cs
================================================
using MiScaleExporter.MAUI.Utils;
namespace MiScaleExporter.MAUI.Behaviors;
public class NumericDoubleValidationBehavior : Behavior
{
protected override void OnAttachedTo(Entry entry)
{
entry.TextChanged += OnEntryTextChanged;
base.OnAttachedTo(entry);
}
protected override void OnDetachingFrom(Entry entry)
{
entry.TextChanged -= OnEntryTextChanged;
base.OnDetachingFrom(entry);
}
void OnEntryTextChanged(object sender, TextChangedEventArgs args)
{
bool isValid = DoubleValueParser.IsValid(args.NewTextValue);
var defaultColor = Application.Current.RequestedTheme == AppTheme.Dark ? Color.FromRgb(255, 255, 255) : Color.FromRgb(0, 0, 0);
((Entry)sender).TextColor = isValid ? defaultColor : Color.FromRgb(255, 0, 0);
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Behaviors/NumericIntValidationBehavior.cs
================================================
namespace MiScaleExporter.MAUI.Behaviors;
public class NumericIntValidationBehavior : Behavior
{
protected override void OnAttachedTo(Entry entry)
{
entry.TextChanged += OnEntryTextChanged;
base.OnAttachedTo(entry);
}
protected override void OnDetachingFrom(Entry entry)
{
entry.TextChanged -= OnEntryTextChanged;
base.OnDetachingFrom(entry);
}
void OnEntryTextChanged(object sender, TextChangedEventArgs args)
{
bool isValid = int.TryParse(args.NewTextValue, out _);
var defaultColor = Application.Current.RequestedTheme == AppTheme.Dark ? Color.FromRgb(255, 255, 255) : Color.FromRgb(0, 0, 0);
((Entry)sender).TextColor = isValid ? defaultColor : Color.FromRgb(255, 0, 0);
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Controls/FlyoutFooter.xaml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/Controls/FlyoutFooter.xaml.cs
================================================
namespace MiScaleExporter.Controls;
public partial class FlyoutFooter : ContentView
{
public FlyoutFooter()
{
InitializeComponent();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Controls/FlyoutHeader.xaml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/Controls/FlyoutHeader.xaml.cs
================================================
namespace MiScaleExporter.Controls;
public partial class FlyoutHeader : ContentView
{
public FlyoutHeader()
{
InitializeComponent();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Converters/InvertedBoolConverter.cs
================================================
using System.Globalization;
namespace MiScaleExporter.MAUI.Converters;
public class InvertedBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return !boolValue;
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return !boolValue;
}
return value;
}
}
================================================
FILE: src/MiScaleExporter.MAUI/MauiProgram.cs
================================================
using CommunityToolkit.Maui;
using Plugin.AdMob;
using Plugin.AdMob.Configuration;
namespace MiScaleExporter.MAUI
{
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp().ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
fonts.AddFont("FontAwesome6Regular.otf", "FontAwesome6Regular");
fonts.AddFont("FontAwesome6Solid.otf", "FontAwesome6Solid");
}).UseMauiCommunityToolkit()
.UseAdMob();
#if DEBUG
//AdConfig.UseTestAdUnitIds = true;
#endif
AdConfig.DefaultBannerAdUnitId = "ca-app-pub-1938975042085430/4160336701";
return builder.Build();
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/MiScaleExporter.MAUI.csproj
================================================
net9.0-androidExeMiScaleExporter.MAUItruetrueenablefalseMiScaleExporter.MAUIcom.lukaszswiderski.miscaleexporter.mauif67014c4-0bdb-47bd-b669-da42bf0d67c41.0114.214.023.010.0.17763.010.0.17763.06.52.4.037TrueFalseapktrue2.4.037apkTrueFalsetrueapkTrueTrueAppSnippets.resx%(Filename)%(Filename)%(Filename)%(Filename)ResXFileCodeGeneratorAppSnippets.Designer.csMSBuild:CompileMSBuild:CompileMSBuild:CompileMSBuild:CompileMSBuild:CompileMSBuild:CompileMSBuild:Compile
================================================
FILE: src/MiScaleExporter.MAUI/Models/BodyComposition.cs
================================================
using System;
using System.Collections.Generic;
using System.Text;
namespace MiScaleExporter.Models
{
public class BodyComposition
{
public double BMI { get; set; }
public double Weight { get; set; }
public double IdealWeight { get; set; }
public double MetabolicAge { get; set; }
public double ProteinPercentage { get; set; }
public double BMR { get; set; }
public double Fat { get; set; }
public double MuscleMass { get; set; }
public double BoneMass { get; set; }
public double VisceralFat { get; set; }
public int BodyType { get; set; }
public double WaterPercentage { get; set; }
public bool IsValid { get; set; }
public bool HasImpedance { get; set; }
public bool IsStabilized { get; set; }
public DateTime Date { get; set; }
public byte[] ReceivedRawData { get; set; }
public string MFACode { get; set; }
public string ExternalApiClientId { get; set; }
public List RawDataLog { get; set; }
public BodyComposition()
{
RawDataLog = new List();
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Models/GarminApiResponse.cs
================================================
namespace MiScaleExporter.Models;
public class GarminApiResponse
{
public bool IsSuccess { get; set; }
public string Message { get; set; }
public bool MFARequested { get; set; }
public string ExternalApiClientId { get; set; }
public string AccessToken { get; set; }
public string TokenSecret { get; set; }
}
================================================
FILE: src/MiScaleExporter.MAUI/Models/GarminBodyCompositionRequest.cs
================================================
namespace MiScaleExporter.Models;
public record GarminBodyCompositionRequest
{
public long TimeStamp { get; set; }
public double Weight { get; set; }
public double PercentFat { get; set; }
public double PercentHydration { get; set; }
public double BoneMass { get; set; }
public double MuscleMass { get; set; }
public double VisceralFatRating{ get; set; }
public int PhysiqueRating { get; set; }
public double MetabolicAge { get; set; }
public double BodyMassIndex { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string ClientID { get; set; }
public string MFACode { get; set; }
public string AccessToken { get; set; }
public string TokenSecret { get; set; }
}
================================================
FILE: src/MiScaleExporter.MAUI/Models/GarminExternalApiResponse.cs
================================================
using YetAnotherGarminConnectClient.Dto;
namespace MiScaleExporter.Models
{
public record GarminExternalApiResponse
{
public string ClientId { get; set; }
public GarminUploadResult UploadResult { get; set; }
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Models/GarminFitFileCreationResult.cs
================================================
namespace MiScaleExporter.Models;
public class GarminFitFileCreationResult
{
public bool IsSuccess { get; set; }
public string Message { get; set; }
public byte[] file { get; set; }
}
================================================
FILE: src/MiScaleExporter.MAUI/Models/GarminUploadResult.cs
================================================
using YetAnotherGarminConnectClient.Dto;
namespace MiScaleExporter.Models
{
public record GarminUploadResult
{
public bool IsSuccess { get; set; }
public long UploadId { get; set; }
public IList Logs { get; set; }
public IList ErrorLogs { get; set; }
public AuthStatus AuthStatus { get; set; }
public bool MFACodeRequested { get; set; }
public string? AccessToken { get; set; }
public string? TokenSecret { get; set; }
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Models/PreferencesKeys.cs
================================================
namespace MiScaleExporter.Models;
public static class PreferencesKeys
{
public static string UserAge = "UserAge";
public static string UserBirthDate = "UserBirthDate";
public static string UseBirthDateMode = "UseBirthDateMode";
public static string UserHeight = "UserHeight";
public static string UserSex = "UserSex";
public static string MiScaleBluetoothAddress = "MiScaleBluetoothAddress";
public static string GarminUserEmail = "GarminUserEmail";
public static string GarminUserSavePassword = "GarminUserSavePassword";
public static string GarminUserPassword = "GarminUserPassword";
public static string GarminUserAccessToken = "GarminUserAccessToken";
public static string GarminUserTokenSecret = "GarminUserTokenSecret";
public static string ApiServerAddressOverride = "ApiServerAddressOverride";
public static string ScaleType = "ScaleType";
public static string OneClickScanAndUpload = "OneClickScanAndUpload";
public static string UseExternalAPI = "UseExternalAPI";
public static string ShowDebugInfo = "ShowDebugInfo";
public static string HideAds = "HideAds";
public static string MuscleMassAsPercentage = "MuscleMassAsPercentage";
public static string S400Bindkey = "S400Bindkey";
public static string DisplayWeightInLbs = "DisplayWeightInLbs";
public static string UseChinaServer = "UseChinaServer";
}
================================================
FILE: src/MiScaleExporter.MAUI/Models/ScaleMeasurement.cs
================================================
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MiScaleExporter.Models
{
public class ScaleMeasurement : INotifyPropertyChanged
{
private ScaleMeasurement() { }
public static ScaleMeasurement Instance { get; } = new ScaleMeasurement();
private string _weight;
public string Weight
{
get => _weight;
set
{
if (_weight != value)
{
_weight = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(Weight)));
}
}
}
private string _foundScale;
public string FoundScale
{
get => _foundScale;
set
{
if (_foundScale != value)
{
_foundScale = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(FoundScale)));
}
}
}
private string _debugData;
public string DebugData
{
get => _debugData;
set
{
if (_debugData != value)
{
_debugData = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(DebugData)));
}
}
}
private string _rawData;
public string RawData
{
get => _rawData;
set
{
if (_rawData != value)
{
_rawData = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(RawData)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Models/ScaleType.cs
================================================
using System;
using System.Collections.Generic;
using System.Text;
namespace MiScaleExporter.Models
{
public enum ScaleType : byte
{
MiBodyCompositionScale = 0,
MiSmartScale = 1,
S400 = 2,
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Models/SettingKeys.cs
================================================
namespace MiScaleExporter.Models;
public static class SettingKeys
{
public static string ApiServerAddress = "https://frog01-20364.wykr.es";
}
================================================
FILE: src/MiScaleExporter.MAUI/Models/Sex.cs
================================================
namespace MiScaleExporter.Models
{
public enum Sex : byte
{
Male = 0,
Female = 1,
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Models/User.cs
================================================
using System;
using System.Collections.Generic;
using System.Text;
namespace MiScaleExporter.Models
{
public class User
{
public int Height { get; set; }
public int Age { get; set; }
public Sex Sex { get; set; }
public ScaleType ScaleType { get; set; }
public string BindKey { get; set; }
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Permission/BluetoothConnectPermission.cs
================================================
using MiScaleExporter.Permission;
using System;
using System.Collections.Generic;
using System.Text;
namespace MiScaleExporter.Droid
{
public class BluetoothConnectPermission : Permissions.BasePlatformPermission, IBluetoothConnectPermission
{
public override (string androidPermission, bool isRuntime)[] RequiredPermissions => new List<(string androidPermission, bool isRuntime)>
{
(Android.Manifest.Permission.BluetoothConnect, true),
(Android.Manifest.Permission.BluetoothScan, true),
(Android.Manifest.Permission.BluetoothAdvertise, true),
}.ToArray();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Permission/IBluetoothConnectPermission.cs
================================================
namespace MiScaleExporter.Permission
{
public interface IBluetoothConnectPermission
{
Task CheckStatusAsync();
Task RequestAsync();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/Android/AndroidManifest.xml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/Android/MainActivity.cs
================================================
using Android.App;
using Android.Content.PM;
using Android.OS;
using Android.Runtime;
using MiScaleExporter.Droid;
using MiScaleExporter.Permission;
namespace MiScaleExporter.MAUI
{
[Activity(Label = "MiScale Exporter", Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Platform.Init(this, savedInstanceState);
DependencyService.Register();
// LoadApplication(app);
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/Android/MainApplication.cs
================================================
using Android.App;
using Android.Runtime;
namespace MiScaleExporter.MAUI
{
[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/Android/Resources/values/colors.xml
================================================
#FFFFFF#007CC3#004b76#007CC3
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/Android/Resources/values/styles.xml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/MacCatalyst/AppDelegate.cs
================================================
using Foundation;
namespace MiScaleExporter.MAUI
{
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/MacCatalyst/Info.plist
================================================
UIDeviceFamily12UIRequiredDeviceCapabilitiesarm64UISupportedInterfaceOrientationsUIInterfaceOrientationPortraitUIInterfaceOrientationLandscapeLeftUIInterfaceOrientationLandscapeRightUISupportedInterfaceOrientations~ipadUIInterfaceOrientationPortraitUIInterfaceOrientationPortraitUpsideDownUIInterfaceOrientationLandscapeLeftUIInterfaceOrientationLandscapeRightXSAppIconAssetsAssets.xcassets/appicon.appiconset
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/MacCatalyst/Program.cs
================================================
using ObjCRuntime;
using UIKit;
namespace MiScaleExporter.MAUI
{
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/Tizen/Main.cs
================================================
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
using System;
namespace MiScaleExporter.MAUI
{
internal class Program : MauiApplication
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
static void Main(string[] args)
{
var app = new Program();
app.Run(args);
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/Tizen/tizen-manifest.xml
================================================
maui-appicon-placeholderhttp://tizen.org/privilege/internet
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/Windows/App.xaml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/Windows/App.xaml.cs
================================================
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace MiScaleExporter.MAUI.WinUI
{
///
/// Provides application-specific behavior to supplement the default Application class.
///
public partial class App : MauiWinUIApplication
{
///
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
///
public App()
{
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/Windows/Package.appxmanifest
================================================
$placeholder$User Name$placeholder$.png
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/Windows/app.manifest
================================================
true/PMPerMonitorV2, PerMonitor
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/iOS/AppDelegate.cs
================================================
using Foundation;
namespace MiScaleExporter.MAUI
{
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/iOS/Info.plist
================================================
LSRequiresIPhoneOSUIDeviceFamily12UIRequiredDeviceCapabilitiesarm64UISupportedInterfaceOrientationsUIInterfaceOrientationPortraitUIInterfaceOrientationLandscapeLeftUIInterfaceOrientationLandscapeRightUISupportedInterfaceOrientations~ipadUIInterfaceOrientationPortraitUIInterfaceOrientationPortraitUpsideDownUIInterfaceOrientationLandscapeLeftUIInterfaceOrientationLandscapeRightXSAppIconAssetsAssets.xcassets/appicon.appiconset
================================================
FILE: src/MiScaleExporter.MAUI/Platforms/iOS/Program.cs
================================================
using ObjCRuntime;
using UIKit;
namespace MiScaleExporter.MAUI
{
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Properties/launchSettings.json
================================================
{
"profiles": {
"Windows Machine": {
"commandName": "MsixPackage",
"nativeDebugging": false
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Resources/Localization/AppSnippets.Designer.cs
================================================
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
//------------------------------------------------------------------------------
namespace MiScaleExporter.MAUI.Resources.Localization {
using System;
///
/// A strongly-typed resource class, for looking up localized strings, etc.
///
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class AppSnippets {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal AppSnippets() {
}
///
/// Returns the cached ResourceManager instance used by this class.
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MiScaleExporter.MAUI.Resources.Localization.AppSnippets", typeof(AppSnippets).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
///
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
///
/// Looks up a localized string similar to This application is an open source community project and I have no affiliation with Garmin..
///
internal static string AboutGarmin {
get {
return ResourceManager.GetString("AboutGarmin", resourceCulture);
}
}
///
/// Looks up a localized string similar to Set the bluetooth address of the scale, and your data in the settings. Stand on your scale. Measure yourself. Mi Body Composition Scale is active up to 15 min after the measurement..
///
internal static string AboutMeasureInstruction {
get {
return ResourceManager.GetString("AboutMeasureInstruction", resourceCulture);
}
}
///
/// Looks up a localized string similar to The application now supports MFA/2FA security features. If you save credentials in settings, authorization tokens will be remembered and you won't be asked every time for MFA code..
///
internal static string AboutMFAAndStoringData {
get {
return ResourceManager.GetString("AboutMFAAndStoringData", resourceCulture);
}
}
///
/// Looks up a localized string similar to Then you can review your data and upload it to Garmin Cloud. If you do not have Mi scale and just want to manually insert the data, you can so..
///
internal static string AboutUpload {
get {
return ResourceManager.GetString("AboutUpload", resourceCulture);
}
}
///
/// Looks up a localized string similar to Age.
///
internal static string Age {
get {
return ResourceManager.GetString("Age", resourceCulture);
}
}
///
/// Looks up a localized string similar to Alternate External API Address.
///
internal static string AlternateExternalAPIAddress {
get {
return ResourceManager.GetString("AlternateExternalAPIAddress", resourceCulture);
}
}
///
/// Looks up a localized string similar to App Settings.
///
internal static string AppSettings {
get {
return ResourceManager.GetString("AppSettings", resourceCulture);
}
}
///
/// Looks up a localized string similar to Basal Metabolism (kCal):.
///
internal static string BasalMetabolosimKCal {
get {
return ResourceManager.GetString("BasalMetabolosimKCal", resourceCulture);
}
}
///
/// Looks up a localized string similar to Bluetooth Address.
///
internal static string BluetoothAddress {
get {
return ResourceManager.GetString("BluetoothAddress", resourceCulture);
}
}
///
/// Looks up a localized string similar to BMI.
///
internal static string BMI {
get {
return ResourceManager.GetString("BMI", resourceCulture);
}
}
///
/// Looks up a localized string similar to Body age (years).
///
internal static string BodyAgeYears {
get {
return ResourceManager.GetString("BodyAgeYears", resourceCulture);
}
}
///
/// Looks up a localized string similar to Body Fat (%).
///
internal static string BodyFatPercent {
get {
return ResourceManager.GetString("BodyFatPercent", resourceCulture);
}
}
///
/// Looks up a localized string similar to Body Type.
///
internal static string BodyType {
get {
return ResourceManager.GetString("BodyType", resourceCulture);
}
}
///
/// Looks up a localized string similar to Bone mass (Lbs).
///
internal static string BoneMassLbs {
get {
return ResourceManager.GetString("BoneMassLbs", resourceCulture);
}
}
///
/// Looks up a localized string similar to Bone mass (Kg).
///
internal static string BoneMassPercent {
get {
return ResourceManager.GetString("BoneMassPercent", resourceCulture);
}
}
///
/// Looks up a localized string similar to Buy me a coffee.
///
internal static string BuyMeACoffee {
get {
return ResourceManager.GetString("BuyMeACoffee", resourceCulture);
}
}
///
/// Looks up a localized string similar to Cancel MFA Request.
///
internal static string CancelMFARequest {
get {
return ResourceManager.GetString("CancelMFARequest", resourceCulture);
}
}
///
/// Looks up a localized string similar to Cancel Search.
///
internal static string CancelSearch {
get {
return ResourceManager.GetString("CancelSearch", resourceCulture);
}
}
///
/// Looks up a localized string similar to Created by Lukasz Swiderski 2023.
///
internal static string CreatedBy {
get {
return ResourceManager.GetString("CreatedBy", resourceCulture);
}
}
///
/// Looks up a localized string similar to Data could not be obtained. try again.
///
internal static string DataCouldNotBeObtained {
get {
return ResourceManager.GetString("DataCouldNotBeObtained", resourceCulture);
}
}
///
/// Looks up a localized string similar to Do you like this app and would you like to support it?.
///
internal static string DoYouLikeAndWouldYouLikeToSupport {
get {
return ResourceManager.GetString("DoYouLikeAndWouldYouLikeToSupport", resourceCulture);
}
}
///
/// Looks up a localized string similar to Email.
///
internal static string Email {
get {
return ResourceManager.GetString("Email", resourceCulture);
}
}
///
/// Looks up a localized string similar to Female.
///
internal static string Female {
get {
return ResourceManager.GetString("Female", resourceCulture);
}
}
///
/// Looks up a localized string similar to Garmin Body Composition Form.
///
internal static string GarminBodyCompositionForm {
get {
return ResourceManager.GetString("GarminBodyCompositionForm", resourceCulture);
}
}
///
/// Looks up a localized string similar to Garmin Form.
///
internal static string GarminForm {
get {
return ResourceManager.GetString("GarminForm", resourceCulture);
}
}
///
/// Looks up a localized string similar to Garmin Settings.
///
internal static string GarminSettings {
get {
return ResourceManager.GetString("GarminSettings", resourceCulture);
}
}
///
/// Looks up a localized string similar to Generate only .fit file.
///
internal static string GenerateOnlyFitFile {
get {
return ResourceManager.GetString("GenerateOnlyFitFile", resourceCulture);
}
}
///
/// Looks up a localized string similar to Go to scan.
///
internal static string GoToScan {
get {
return ResourceManager.GetString("GoToScan", resourceCulture);
}
}
///
/// Looks up a localized string similar to Height.
///
internal static string Height {
get {
return ResourceManager.GetString("Height", resourceCulture);
}
}
///
/// Looks up a localized string similar to Help.
///
internal static string Help {
get {
return ResourceManager.GetString("Help", resourceCulture);
}
}
///
/// Looks up a localized string similar to 8. Coffee.
///
internal static string HelpCoffee {
get {
return ResourceManager.GetString("HelpCoffee", resourceCulture);
}
}
///
/// Looks up a localized string similar to Keep in mind that this is my hobby project. It was created for my inner needs and I believe it may be useful to others as well. I do not receive any compensation for creating and maintaining this project. If you want to support the development or thank me, you can buy me a coffee..
///
internal static string HelpCoffeeDescription {
get {
return ResourceManager.GetString("HelpCoffeeDescription", resourceCulture);
}
}
///
/// Looks up a localized string similar to 1. How to connect to the scale.
///
internal static string HelpConnect {
get {
return ResourceManager.GetString("HelpConnect", resourceCulture);
}
}
///
/// Looks up a localized string similar to Set the Bluetooth address of the scale in settings. Set the correct age/height/gender values. Measure yourself. The scale is active for up to 15 minutes after measurement..
///
internal static string HelpConnectDescription {
get {
return ResourceManager.GetString("HelpConnectDescription", resourceCulture);
}
}
///
/// Looks up a localized string similar to Bluetooth address can be found in Zepp Life > Profile > My devices > Mi Body Composition Scale > Bluetooth address (hold to copy).
///
internal static string HelpConnectDescription2 {
get {
return ResourceManager.GetString("HelpConnectDescription2", resourceCulture);
}
}
///
/// Looks up a localized string similar to 5. External Api.
///
internal static string HelpExternalApi {
get {
return ResourceManager.GetString("HelpExternalApi", resourceCulture);
}
}
///
/// Looks up a localized string similar to External API is used as a proxy for older Android phones (9 or lower). The default address is https://frog01-20364.wykr.es/. You can host your own version of the API and this app will send the data to your system instead of Garmin. Check out the project repository for more information..
///
internal static string HelpExternalApiDescription {
get {
return ResourceManager.GetString("HelpExternalApiDescription", resourceCulture);
}
}
///
/// Looks up a localized string similar to 7. Feedback & Contact.
///
internal static string HelpFeedback {
get {
return ResourceManager.GetString("HelpFeedback", resourceCulture);
}
}
///
/// Looks up a localized string similar to If you found a bug, need a feature or have an idea, share it with me at the project repository..
///
internal static string HelpFeedbackDescription {
get {
return ResourceManager.GetString("HelpFeedbackDescription", resourceCulture);
}
}
///
/// Looks up a localized string similar to 6. Garmin and Xiaomi.
///
internal static string HelpGarminAndXiaomi {
get {
return ResourceManager.GetString("HelpGarminAndXiaomi", resourceCulture);
}
}
///
/// Looks up a localized string similar to This application is an open source community project and I have no affiliation with Garmin or Xiaomi. You use it at your own risk. You can always check out the source code on Github..
///
internal static string HelpGarminAndXiaomiDescription {
get {
return ResourceManager.GetString("HelpGarminAndXiaomiDescription", resourceCulture);
}
}
///
/// Looks up a localized string similar to 2. How It works.
///
internal static string HelpHowItWorks {
get {
return ResourceManager.GetString("HelpHowItWorks", resourceCulture);
}
}
///
/// Looks up a localized string similar to This App pass your data, email and password directly to Garmin Cloud or via external API server. The API does not store or log anything, it's just a middleware between this App and Garmin services..
///
internal static string HelpHowItWorksDescription {
get {
return ResourceManager.GetString("HelpHowItWorksDescription", resourceCulture);
}
}
///
/// Looks up a localized string similar to 3. Strange results? (Like 10kg only).
///
internal static string HelpStrangeResults {
get {
return ResourceManager.GetString("HelpStrangeResults", resourceCulture);
}
}
///
/// Looks up a localized string similar to If you have the Mi Body Composition Scale 2. Disable the 'Weight small object' option in Zepp Life.
///
internal static string HelpStrangeResultsDescription {
get {
return ResourceManager.GetString("HelpStrangeResultsDescription", resourceCulture);
}
}
///
/// Looks up a localized string similar to 4. Results different from Zepp Life.
///
internal static string HelpZeppLifeDriference {
get {
return ResourceManager.GetString("HelpZeppLifeDriference", resourceCulture);
}
}
///
/// Looks up a localized string similar to Please check the age/height/gender parameters - should be identical in both apps. But even then, the values may be slightly different because we do not use the exact algorithm used by Xiaomi, but an alternative open source algorithm..
///
internal static string HelpZeppLifeDriferenceDescription {
get {
return ResourceManager.GetString("HelpZeppLifeDriferenceDescription", resourceCulture);
}
}
///
/// Looks up a localized string similar to Ideal Weight (Kg):.
///
internal static string IdealWeightKg {
get {
return ResourceManager.GetString("IdealWeightKg", resourceCulture);
}
}
///
/// Looks up a localized string similar to Ideal Weight (Lbs):.
///
internal static string IdealWeightLbs {
get {
return ResourceManager.GetString("IdealWeightLbs", resourceCulture);
}
}
///
/// Looks up a localized string similar to Male.
///
internal static string Male {
get {
return ResourceManager.GetString("Male", resourceCulture);
}
}
///
/// Looks up a localized string similar to MFA Code.
///
internal static string MFACode {
get {
return ResourceManager.GetString("MFACode", resourceCulture);
}
}
///
/// Looks up a localized string similar to Mi Body Composition Scale 1 / 2.
///
internal static string MiBodyCompositionScale {
get {
return ResourceManager.GetString("MiBodyCompositionScale", resourceCulture);
}
}
///
/// Looks up a localized string similar to Mi Scale.
///
internal static string MiScale {
get {
return ResourceManager.GetString("MiScale", resourceCulture);
}
}
///
/// Looks up a localized string similar to Mi Scale Data.
///
internal static string MiScaleData {
get {
return ResourceManager.GetString("MiScaleData", resourceCulture);
}
}
///
/// Looks up a localized string similar to Mi Scale Exporter.
///
internal static string MiScaleExporter {
get {
return ResourceManager.GetString("MiScaleExporter", resourceCulture);
}
}
///
/// Looks up a localized string similar to Mi Smart Scale.
///
internal static string MiSmartScale {
get {
return ResourceManager.GetString("MiSmartScale", resourceCulture);
}
}
///
/// Looks up a localized string similar to Muscle mass as %.
///
internal static string MuscleMassAsPercentage {
get {
return ResourceManager.GetString("MuscleMassAsPercentage", resourceCulture);
}
}
///
/// Looks up a localized string similar to Muscle Mass (Kg).
///
internal static string MuscleMassKg {
get {
return ResourceManager.GetString("MuscleMassKg", resourceCulture);
}
}
///
/// Looks up a localized string similar to Muscle Mass (Lbs).
///
internal static string MuscleMassLbs {
get {
return ResourceManager.GetString("MuscleMassLbs", resourceCulture);
}
}
///
/// Looks up a localized string similar to Muscle Mass (%).
///
internal static string MuscleMassPer {
get {
return ResourceManager.GetString("MuscleMassPer", resourceCulture);
}
}
///
/// Looks up a localized string similar to Not found.
///
internal static string NotFound {
get {
return ResourceManager.GetString("NotFound", resourceCulture);
}
}
///
/// Looks up a localized string similar to OK.
///
internal static string OK {
get {
return ResourceManager.GetString("OK", resourceCulture);
}
}
///
/// Looks up a localized string similar to One click - Scan & Upload.
///
internal static string OneClickScanUpload {
get {
return ResourceManager.GetString("OneClickScanUpload", resourceCulture);
}
}
///
/// Looks up a localized string similar to Open Help.
///
internal static string OpenHelp {
get {
return ResourceManager.GetString("OpenHelp", resourceCulture);
}
}
///
/// Looks up a localized string similar to Password.
///
internal static string Password {
get {
return ResourceManager.GetString("Password", resourceCulture);
}
}
///
/// Looks up a localized string similar to Permission to use Bluetooth is required to scan..
///
internal static string PermissionBluetoothRequired {
get {
return ResourceManager.GetString("PermissionBluetoothRequired", resourceCulture);
}
}
///
/// Looks up a localized string similar to Permission to use Location (Bluetooth) is required to scan..
///
internal static string PermissionLocationRequired {
get {
return ResourceManager.GetString("PermissionLocationRequired", resourceCulture);
}
}
///
/// Looks up a localized string similar to Problem.
///
internal static string Problem {
get {
return ResourceManager.GetString("Problem", resourceCulture);
}
}
///
/// Looks up a localized string similar to Protein (%):.
///
internal static string ProteinPercent {
get {
return ResourceManager.GetString("ProteinPercent", resourceCulture);
}
}
///
/// Looks up a localized string similar to Reset to Default.
///
internal static string ResetToDefault {
get {
return ResourceManager.GetString("ResetToDefault", resourceCulture);
}
}
///
/// Looks up a localized string similar to Response.
///
internal static string Response {
get {
return ResourceManager.GetString("Response", resourceCulture);
}
}
///
/// Looks up a localized string similar to Scale Model.
///
internal static string ScaleModel {
get {
return ResourceManager.GetString("ScaleModel", resourceCulture);
}
}
///
/// Looks up a localized string similar to Scale Settings.
///
internal static string ScaleSettings {
get {
return ResourceManager.GetString("ScaleSettings", resourceCulture);
}
}
///
/// Looks up a localized string similar to Scanning....
///
internal static string Scanning {
get {
return ResourceManager.GetString("Scanning", resourceCulture);
}
}
///
/// Looks up a localized string similar to Settings.
///
internal static string Settings {
get {
return ResourceManager.GetString("Settings", resourceCulture);
}
}
///
/// Looks up a localized string similar to Hide Ads Banner.
///
internal static string SettingsHideAds {
get {
return ResourceManager.GetString("SettingsHideAds", resourceCulture);
}
}
///
/// Looks up a localized string similar to Sex.
///
internal static string Sex {
get {
return ResourceManager.GetString("Sex", resourceCulture);
}
}
///
/// Looks up a localized string similar to Show Debug Info during scan.
///
internal static string ShowDebug {
get {
return ResourceManager.GetString("ShowDebug", resourceCulture);
}
}
///
/// Looks up a localized string similar to Start.
///
internal static string Start {
get {
return ResourceManager.GetString("Start", resourceCulture);
}
}
///
/// Looks up a localized string similar to Stop Measure.
///
internal static string StopMeasure {
get {
return ResourceManager.GetString("StopMeasure", resourceCulture);
}
}
///
/// Looks up a localized string similar to Time of measurement.
///
internal static string TimeOfMeasurement {
get {
return ResourceManager.GetString("TimeOfMeasurement", resourceCulture);
}
}
///
/// Looks up a localized string similar to Uploaded.
///
internal static string Uploaded {
get {
return ResourceManager.GetString("Uploaded", resourceCulture);
}
}
///
/// Looks up a localized string similar to Uploading....
///
internal static string Uploading {
get {
return ResourceManager.GetString("Uploading", resourceCulture);
}
}
///
/// Looks up a localized string similar to Upload to Garmin Cloud.
///
internal static string UploadToGarminCloud {
get {
return ResourceManager.GetString("UploadToGarminCloud", resourceCulture);
}
}
///
/// Looks up a localized string similar to Use External API.
///
internal static string UseExternalAPI {
get {
return ResourceManager.GetString("UseExternalAPI", resourceCulture);
}
}
///
/// Looks up a localized string similar to Values that will not be sent to the cloud:.
///
internal static string ValuesThatWillNotBeSentToTheCloud {
get {
return ResourceManager.GetString("ValuesThatWillNotBeSentToTheCloud", resourceCulture);
}
}
///
/// Looks up a localized string similar to Visceral fat.
///
internal static string VisceralFat {
get {
return ResourceManager.GetString("VisceralFat", resourceCulture);
}
}
///
/// Looks up a localized string similar to Visit the project repository.
///
internal static string VisitProjectRepository {
get {
return ResourceManager.GetString("VisitProjectRepository", resourceCulture);
}
}
///
/// Looks up a localized string similar to Water (%).
///
internal static string WaterPercent {
get {
return ResourceManager.GetString("WaterPercent", resourceCulture);
}
}
///
/// Looks up a localized string similar to Weight (Kg).
///
internal static string WeightKg {
get {
return ResourceManager.GetString("WeightKg", resourceCulture);
}
}
///
/// Looks up a localized string similar to Weight (Lbs).
///
internal static string WeightLbs {
get {
return ResourceManager.GetString("WeightLbs", resourceCulture);
}
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Resources/Localization/AppSnippets.pl.resx
================================================
text/microsoft-resx2.0System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Ta aplikacja jest projektem społeczności open source i nie ma żadnych powiązań z firmą Garmin.Ustaw w ustawieniach adres bluetooth wagi, oraz swoje dane. Stań na swojej wadze. Zmierz się. Mi Body Composition Scale jest aktywna do 15 min po dokonaniu pomiaru.Aplikacja obsługuje teraz funkcje bezpieczeństwa MFA/2FA. Jeśli zapiszesz dane uwierzytelniające w ustawieniach, tokeny autoryzacyjne zostaną zapamiętane i nie będziesz za każdym razem proszony o podanie kodu MFA.Następnie możesz przejrzeć swoje dane i przesłać je do Garmin Cloud. Jeśli nie masz wagi Mi Scale i możesz po prostu ręcznie wprowadzić dane i przesłac je do Garmin Connect.WiekAlternatywny zewnętrzny adres APIUstawienia aplikacjiMetabolizm podstawowy (kCal):Adres BluetoothBMIWiek ciała (w latach)Tłuszcz w organizmie (%)Typ CiałaMasa kostna (Kg)Masa kostna (Lbs)Postaw mi kawęAnuluj MFAAnuluj wyszukiwanieStworzone przez Łukasza Świderskiego 2023Nie udało się uzyskać danych. Spróbuj ponownieCzy podoba Ci się ta aplikacja i chciałbyś ją wesprzeć?EmailKobietaFormularz składu ciała GarminFormularz GarminUstawienia GarminGeneruj tylko plik .fitPrzejdź do skanowaniaWzrostPomoc8. KawaNależy pamiętać, że jest to mój projekt hobbystyczny. Został on stworzony dla moich wewnętrznych potrzeb i wierzę, że może być przydatny również dla innych. Nie otrzymuję żadnego wynagrodzenia za stworzenie i utrzymanie tego projektu. Jeśli chcesz wesprzeć rozwój lub podziękować mi, możesz postawić mi kawę.1. Jak podłączyć się do wagiUstawić adres Bluetooth wagi w ustawieniach. Ustaw prawidłowe wartości wieku/wzrostu/płci. Zmierz się. Waga jest aktywna i wysyła wyniki do 15 minut po pomiarze.Adres Bluetooth można znaleźć w Zepp Life > Profil > Moje urządzenia > Mi Body Composition Scale > Adres Bluetooth (przytrzymaj, aby skopiować)5. Zewnętrzne ApiZewnętrzny interfejs API jest używany jako proxy dla starszych telefonów z systemem Android (9 lub niższy). Domyślny adres to https://frog01-20364.wykr.es/. Możesz hostować własną wersję API, a ta aplikacja wyśle dane do twojego systemu zamiast do Garmina. Sprawdź repozytorium projektu, aby uzyskać więcej informacji.7. Informacje zwrotne i kontaktJeśli znalazłeś błąd, potrzebujesz jakiejś funkcji lub masz pomysł, podziel się nim ze mną w repozytorium projektu.6. Garmin i XiaomiTa aplikacja jest otwarto-źródłowym projektem społeczności i nie ma żadnych powiązań z Garminem lub Xiaomi. Używasz jej na własne ryzyko. Zawsze możesz sprawdzić kod źródłowy na Githubie.2. Jak to działaTa aplikacja przekazuje Twoje dane, email i hasło bezpośrednio do Garmin Cloud lub poprzez zewnętrzny serwer API. API nie przechowuje ani nie rejestruje niczego, jest to tylko pośrednikiem pomiędzy aplikacją a usługami firmy Garmin.3. Dziwne wyniki? (Np zwraca małe wartości jak 10kg)Jeśli posiadasz Mi Body Composition Scale 2. Wyłącz opcję "Waż mały obiekt" w Zepp Life4. Wyniki różne od Zepp LifeSprawdź parametry wieku/wzrostui/płci - powinny być identyczne w obu aplikacjach. Ale nawet wtedy wartości mogą być nieco inne, ponieważ nie używamy dokładnie takiego samego algorytmu jak Xiaomi, ale alternatywnego algorytmu open source.Idealna masa ciała (Kg):Idealna masa ciała (Lbs):MężczyznaKod MFAMi Body Composition Scale 1 / 2Mi ScaleDane Mi ScaleMi Scale ExporterMi Smart ScaleMasa mięśniowa jako %Masa mięśniowa (Kg)Masa mięśniowa (Lbs)Masa mięśniowa (%)Nie znalezionoOKJedno kliknięcie - skanowanie i przesyłanieOtwórz pomocHasłoDo skanowania wymagana jest zgoda na korzystanie z Bluetooth.Do skanowania wymagane jest pozwolenie na korzystanie z funkcji Lokalizacji (Bluetooth).Bluetooth jest wyłączony. Włącz Bluetooth, aby skanować wagę Mi Scale.ProblemBiałko (%):Przywrócenie ustawień domyślnychOdpowiedźModel WagiUstawienia wagiSkanowanie...UstawieniaSchowaj reklamyPłećPokaż informacje debug podczas skanowaniaStartZatrzymaj pomiarCzas pomiaruPrzesłanoWysyłanie...Prześlij do usługi Garmin CloudUżyj zewnętrznego APIWartości, które nie będą wysyłane do chmury:Tłuszcz trzewnyOdwiedź repozytorium projektuWoda (%)Waga (Kg)Waga (Lbs)
================================================
FILE: src/MiScaleExporter.MAUI/Resources/Localization/AppSnippets.resx
================================================
text/microsoft-resx2.0System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089This application is an open source community project and I have no affiliation with Garmin.Set the bluetooth address of the scale, and your data in the settings. Stand on your scale. Measure yourself. Mi Body Composition Scale is active up to 15 min after the measurement.The application now supports MFA/2FA security features. If you save credentials in settings, authorization tokens will be remembered and you won't be asked every time for MFA code.Then you can review your data and upload it to Garmin Cloud. If you do not have Mi scale and just want to manually insert the data, you can so.AgeAlternate External API AddressApp SettingsBasal Metabolism (kCal):Bluetooth AddressBMIBody age (years)Body Fat (%)Body TypeBone mass (Kg)Bone mass (Lbs)Buy me a coffeeCancel MFA RequestCancel SearchCreated by Lukasz Swiderski 2023Data could not be obtained. try againDo you like this app and would you like to support it?EmailFemaleGarmin Body Composition FormGarmin FormGarmin SettingsGenerate only .fit fileGo to scanHeightHelp8. CoffeeKeep in mind that this is my hobby project. It was created for my inner needs and I believe it may be useful to others as well. I do not receive any compensation for creating and maintaining this project. If you want to support the development or thank me, you can buy me a coffee.1. How to connect to the scaleSet the Bluetooth address of the scale in settings. Set the correct age/height/gender values. Measure yourself. The scale is active for up to 15 minutes after measurement.Bluetooth address can be found in Zepp Life > Profile > My devices > Mi Body Composition Scale > Bluetooth address (hold to copy)5. External ApiExternal API is used as a proxy for older Android phones (9 or lower). The default address is https://frog01-20364.wykr.es/. You can host your own version of the API and this app will send the data to your system instead of Garmin. Check out the project repository for more information.7. Feedback & ContactIf you found a bug, need a feature or have an idea, share it with me at the project repository.6. Garmin and XiaomiThis application is an open source community project and I have no affiliation with Garmin or Xiaomi. You use it at your own risk. You can always check out the source code on Github.2. How It worksThis App pass your data, email and password directly to Garmin Cloud or via external API server. The API does not store or log anything, it's just a middleware between this App and Garmin services.3. Strange results? (Like 10kg only)If you have the Mi Body Composition Scale 2. Disable the 'Weight small object' option in Zepp Life4. Results different from Zepp LifePlease check the age/height/gender parameters - should be identical in both apps. But even then, the values may be slightly different because we do not use the exact algorithm used by Xiaomi, but an alternative open source algorithm.Ideal Weight (Kg):Ideal Weight (Lbs):MaleMFA CodeMi Body Composition Scale 1 / 2Mi ScaleMi Scale DataMi Scale ExporterMi Smart ScaleMuscle mass as %Muscle Mass (Kg)Muscle Mass (Lbs)Muscle Mass (%)Not foundOKOne click - Scan & UploadOpen HelpPasswordPermission to use Bluetooth is required to scan.Permission to use Location (Bluetooth) is required to scan.Bluetooth is disabled. Please enable Bluetooth to scan for your Mi Scale.ProblemProtein (%):Reset to DefaultResponseScale ModelScale SettingsScanning...SettingsHide Ads BannerSexShow Debug Info during scanStartStop MeasureTime of measurementUploadedUploading...Upload to Garmin CloudUse External APIValues that will not be sent to the cloud:Visceral fatVisit the project repositoryWater (%)Weight (Kg)Weight (Lbs)
================================================
FILE: src/MiScaleExporter.MAUI/Resources/Raw/AboutAssets.txt
================================================
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories). Deployment of the asset to your application
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
These files will be deployed with you package and will be accessible using Essentials:
async Task LoadMauiAsset()
{
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
using var reader = new StreamReader(stream);
var contents = reader.ReadToEnd();
}
================================================
FILE: src/MiScaleExporter.MAUI/Resources/Styles/Colors.xaml
================================================
#007CC3#DFD8F7#004b76WhiteBlack#E1E1E1#C8C8C8#ACACAC#919191#6E6E6E#404040#212121#141414#6E6E6EDarkRed#F7B548#FFD590#FFE5B9#28C2D1#7BDDEF#C3F2F4#3E8EED#72ACF1#A7CBF6
================================================
FILE: src/MiScaleExporter.MAUI/Resources/Styles/Styles.xaml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/Services/DataInterpreter.cs
================================================
using MiScaleBodyComposition.Contracts;
using MiScaleExporter.Models;
namespace MiScaleExporter.Services
{
public class DataInterpreter : IDataInterpreter
{
private void ValidateAesKey(string aesKey)
{
if (string.IsNullOrEmpty(aesKey) || aesKey.Length != 32)
{
throw new ArgumentException("AES key must be a 32-character hexadecimal string.");
}
}
private void ValidateBluetoothAddress(string btAddress)
{
if (string.IsNullOrEmpty(btAddress) || btAddress.Length != 17 || !btAddress.All(c => char.IsLetterOrDigit(c) || c == ':'))
{
throw new ArgumentException("Bluetooth address must be a valid 17-character string in the format XX:XX:XX:XX:XX:XX.");
}
}
public BodyComposition ComputeData(byte[] data, User _user, string btAddress)
{
var user = new MiScaleBodyComposition.User(_user.Height, _user.Age, (MiScaleBodyComposition.Sex)(byte)_user.Sex);
switch (_user.ScaleType)
{
case ScaleType.MiBodyCompositionScale:
var miBodyCompositionScale = new MiScaleBodyComposition.MiScale();
var isStabilized = miBodyCompositionScale.Istabilized(data, user);
var hasImpedance = miBodyCompositionScale.HasImpedance(data, user);
if (data.Length > 13)
{
data = data.Skip(data.Length - 13).ToArray();
}
if (isStabilized)
{
var bc = miBodyCompositionScale.GetBodyComposition(data, user, true);
var bodyComposition = new BodyComposition
{
Weight = bc.Weight,
BMI = bc.BMI,
ProteinPercentage = bc.ProteinPercentage,
IdealWeight = bc.IdealWeight,
BMR = bc.BMR,
BoneMass = bc.BoneMass,
Fat = bc.Fat,
MetabolicAge = bc.MetabolicAge,
MuscleMass = bc.MuscleMass,
VisceralFat = bc.VisceralFat,
WaterPercentage = bc.Water,
BodyType = bc.BodyType,
HasImpedance = hasImpedance,
IsStabilized = isStabilized,
Date = bc.Date,
};
return bodyComposition;
}
else
{
var bodyComposition = new BodyComposition
{
Weight = GetWeight(data),
HasImpedance = hasImpedance,
IsStabilized = isStabilized,
};
return bodyComposition;
}
case ScaleType.MiSmartScale:
var legacyMiscale = new MiScaleBodyComposition.LegacyMiScale();
if (legacyMiscale.Istabilized(data))
{
var legacyResult = legacyMiscale.GetWeight(data, _user.Height, true);
var bodyComposition = new BodyComposition
{
Weight = legacyResult.Weight,
BMI = legacyResult.BMI,
Date = legacyResult.Date,
IsStabilized = true
};
return bodyComposition;
}
else
{
return null;
}
case ScaleType.S400:
if(data.Length == 26)
{
this.ValidateAesKey(_user.BindKey);
this.ValidateBluetoothAddress(btAddress);
var s400Scale = new MiScaleBodyComposition.S400Scale();
var s400Result = s400Scale.GetBodyComposition(user, new S400InputData
{
Data = data,
AesKey = _user.BindKey,
MacOriginal = btAddress,
});
if (s400Result != null)
{
var bodyComposition = new BodyComposition
{
Weight = s400Result.Weight,
BMI = s400Result.BMI,
ProteinPercentage = s400Result.ProteinPercentage,
IdealWeight = s400Result.IdealWeight,
BMR = s400Result.BMR,
BoneMass = s400Result.BoneMass,
Fat = s400Result.Fat,
MetabolicAge = s400Result.MetabolicAge,
MuscleMass = s400Result.MuscleMass,
VisceralFat = s400Result.VisceralFat,
WaterPercentage = s400Result.Water,
BodyType = s400Result.BodyType,
HasImpedance = true,
IsStabilized = true,
Date = s400Result.Date,
};
return bodyComposition;
}
}
var emptyBC = new BodyComposition
{
Weight = 0,
HasImpedance = false,
IsStabilized = false,
};
return emptyBC;
default:
throw new NotImplementedException();
}
}
private double GetWeight(byte[] data)
{
return (double)(((data[12] & 0xFF) << 8) | (data[11] & 0xFF)) * 0.005;
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Services/GarminService.cs
================================================
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Android.Net;
using Microsoft.Extensions.Logging;
using MiScaleExporter.Models;
using Newtonsoft.Json;
using NLog;
using NLog.Extensions.Logging;
using Xamarin.Android.Net;
using YetAnotherGarminConnectClient;
using YetAnotherGarminConnectClient.Dto.Garmin.Fit;
namespace MiScaleExporter.Services;
public class GarminService : IGarminService
{
private HttpClient _httpClient;
private ILogService _logService;
private Microsoft.Extensions.Logging.ILogger _logger;
private IClient _garminClient;
public GarminService(ILogService logService)
{
_logService = logService;
var configuration = LogService.CreateLogger();
using (ILoggerFactory factory = LoggerFactory.Create(builder =>
builder.AddNLog(configuration))
)
{
_logger = factory.CreateLogger();
}
System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
if (DeviceInfo.Platform == DevicePlatform.Android)
{
_httpClient = new HttpClient(new AndroidMessageHandler())
{
Timeout = TimeSpan.FromMinutes(5),
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
DefaultRequestVersion = HttpVersion.Version11,
};
}
else
{
_httpClient = new HttpClient()
{
Timeout = TimeSpan.FromMinutes(5),
};
}
}
public async Task UploadAsync(BodyComposition bodyComposition, DateTime time, CredentialsData credencials)
{
if (DeviceInfo.Platform == DevicePlatform.Android) //Older android versions do not support TLS 1.3, so force to use external api for them
{
if (DeviceInfo.Version.Major < 10)
{
Preferences.Set(PreferencesKeys.UseExternalAPI, true);
}
}
if (Preferences.Get(PreferencesKeys.UseExternalAPI, false))
{
return await UploadViaExternalAPIAsync(bodyComposition, time, credencials);
}
else
{
return await UploadViaDirectCallToGarminAsync(bodyComposition, time, credencials);
}
}
private async Task UploadViaDirectCallToGarminAsync(BodyComposition bodyComposition, DateTime time, CredentialsData credencials)
{
var result = new GarminApiResponse();
try
{
var userProfileSettings = new UserProfileSettings
{
Age = Preferences.Get(PreferencesKeys.UserAge, 25),
Height = Preferences.Get(PreferencesKeys.UserHeight, 170),
};
var scaleDTO = new GarminWeightScaleDTO
{
TimeStamp = time,
Weight = Convert.ToSingle(bodyComposition.Weight),
PercentFat = Convert.ToSingle(bodyComposition.Fat),
PercentHydration = Convert.ToSingle(bodyComposition.WaterPercentage),
BoneMass = Convert.ToSingle(bodyComposition.BoneMass),
MuscleMass = Convert.ToSingle(bodyComposition.MuscleMass),
VisceralFatRating = Convert.ToByte(bodyComposition.VisceralFat),
VisceralFatMass = Convert.ToSingle(bodyComposition.VisceralFat),
PhysiqueRating = Convert.ToByte(bodyComposition.BodyType),
MetabolicAge = Convert.ToByte(bodyComposition.MetabolicAge),
BodyMassIndex = Convert.ToSingle(bodyComposition.BMI),
};
if (string.IsNullOrEmpty(bodyComposition.MFACode))
{
var useChinaServer = Preferences.Get(PreferencesKeys.UseChinaServer, false);
var garminServer = useChinaServer
? YetAnotherGarminConnectClient.Dto.GarminServer.CHINA
: YetAnotherGarminConnectClient.Dto.GarminServer.GLOBAL;
_garminClient = await ClientFactory.Create(garminServer);
}
var garminApiReponse = await _garminClient.UploadWeight(scaleDTO, userProfileSettings, credencials, bodyComposition.MFACode);
var logs = LogService.GetLogs();
var errorlogs = LogService.GetErrorLogs();
result.IsSuccess = garminApiReponse.IsSuccess;
result.MFARequested = garminApiReponse.MFACodeRequested;
result.AccessToken = garminApiReponse.AccessToken;
result.TokenSecret = garminApiReponse.TokenSecret;
if (result.MFARequested)
{
result.Message = "Please provide MFA/2FA Code";
}
if (!result.IsSuccess && !result.MFARequested)
{
var errorMessage = garminApiReponse?.ErrorLogs?.FirstOrDefault() ?? errorlogs?.FirstOrDefault() ?? "Error";
throw new Exception(errorMessage);
}
return result;
}
catch (Exception ex)
{
var logs = LogService.GetLogs();
var errorlogs = LogService.GetErrorLogs();
_logService.LogError(ex?.Message);
result.Message = ex.Message;
return result;
}
}
public async Task GenerateFitFileAsync(BodyComposition bodyComposition, DateTime time)
{
var result = new GarminFitFileCreationResult();
try
{
var userProfileSettings = new UserProfileSettings
{
Age = Preferences.Get(PreferencesKeys.UserAge, 25),
Height = Preferences.Get(PreferencesKeys.UserHeight, 170),
};
var scaleDTO = new GarminWeightScaleDTO
{
TimeStamp = time,
Weight = Convert.ToSingle(bodyComposition.Weight),
PercentFat = Convert.ToSingle(bodyComposition.Fat),
PercentHydration = Convert.ToSingle(bodyComposition.WaterPercentage),
BoneMass = Convert.ToSingle(bodyComposition.BoneMass),
MuscleMass = Convert.ToSingle(bodyComposition.MuscleMass),
VisceralFatRating = Convert.ToByte(bodyComposition.VisceralFat),
VisceralFatMass = Convert.ToSingle(bodyComposition.VisceralFat),
PhysiqueRating = Convert.ToByte(bodyComposition.BodyType),
MetabolicAge = Convert.ToByte(bodyComposition.MetabolicAge),
BodyMassIndex = Convert.ToSingle(bodyComposition.BMI),
};
_garminClient = await ClientFactory.Create();
var file = _garminClient.GenerateWeightFitFile(scaleDTO, userProfileSettings);
var logs = LogService.GetLogs();
var errorlogs = LogService.GetErrorLogs();
if(errorlogs.Count > 0)
{
result.Message = string.Join(Environment.NewLine, errorlogs);
}
result.IsSuccess = file != null;
result.file = file;
return result;
}
catch (Exception ex)
{
var logs = LogService.GetLogs();
var errorlogs = LogService.GetErrorLogs();
_logService.LogError(ex?.Message);
result.Message = ex.Message;
return result;
}
}
private async Task UploadViaExternalAPIAsync(BodyComposition bodyComposition, DateTime time, CredentialsData credencials)
{
var unixTime = ((DateTimeOffset)time).ToUnixTimeSeconds();
var request = new GarminBodyCompositionRequest
{
Email = credencials.Email,
Password = credencials.Password,
AccessToken = credencials.AccessToken,
TokenSecret = credencials.TokenSecret,
Weight = bodyComposition.Weight,
BoneMass = bodyComposition.BoneMass,
MuscleMass = bodyComposition.MuscleMass,
MetabolicAge = bodyComposition.MetabolicAge,
PercentFat = bodyComposition.Fat,
VisceralFatRating = bodyComposition.VisceralFat,
BodyMassIndex = bodyComposition.BMI,
PercentHydration = bodyComposition.WaterPercentage,
PhysiqueRating = bodyComposition.BodyType,
TimeStamp = unixTime,
};
if (!string.IsNullOrEmpty(bodyComposition.ExternalApiClientId))
{
request.MFACode = bodyComposition.MFACode;
request.ClientID = bodyComposition.ExternalApiClientId;
}
return await UploadToGarminCloud(request);
}
private async Task UploadToGarminCloud(GarminBodyCompositionRequest request)
{
var result = new GarminApiResponse();
try
{
var dataAsString = JsonConvert.SerializeObject(request);
var content = new StringContent(dataAsString, Encoding.UTF8, "application/json");
var response = await PostAsync("/upload", content);
using (var stream = await response.Content.ReadAsStreamAsync())
using (var reader = new StreamReader(stream))
{
var message = await reader.ReadToEndAsync();
if (response.IsSuccessStatusCode)
{
var apiResponse = JsonConvert.DeserializeObject(message);
result.AccessToken = apiResponse?.UploadResult?.AccessToken;
result.TokenSecret = apiResponse?.UploadResult?.TokenSecret;
if (apiResponse?.UploadResult?.AuthStatus == YetAnotherGarminConnectClient.Dto.AuthStatus.MFARedirected)
{
result.Message = "Please provide MFA/2FA Code";
result.MFARequested = true;
result.ExternalApiClientId = apiResponse.ClientId;
return result;
}
}
result.ExternalApiClientId = null;
result.IsSuccess = response.IsSuccessStatusCode;
result.Message = message;
return result;
}
}
catch (Exception ex)
{
_logService.LogError(ex.Message);
result.Message = ex.Message;
return result;
}
}
private async Task PostAsync(string requestUri, HttpContent content)
{
var baseAddress = Preferences.Get(PreferencesKeys.ApiServerAddressOverride, SettingKeys.ApiServerAddress);
var response = await _httpClient.PostAsync($"{baseAddress}{requestUri}", content);
return response;
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Services/IDataInterpreter.cs
================================================
using MiScaleExporter.Models;
namespace MiScaleExporter.Services
{
public interface IDataInterpreter
{
BodyComposition ComputeData(byte[] data, User _user, string btAddress);
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Services/IGarminService.cs
================================================
using System;
using System.Threading.Tasks;
using MiScaleExporter.Models;
using YetAnotherGarminConnectClient.Dto.Garmin.Fit;
namespace MiScaleExporter.Services;
public interface IGarminService
{
Task UploadAsync(BodyComposition bodyComposition, DateTime time, CredentialsData credencials);
Task GenerateFitFileAsync(BodyComposition bodyComposition, DateTime time);
}
================================================
FILE: src/MiScaleExporter.MAUI/Services/ILogService.cs
================================================
using System.Reflection;
namespace MiScaleExporter.Services;
public interface ILogService
{
void LogDebug(string message);
void LogError(string message);
void LogFatal(string message);
void LogInfo(string message);
void LogWarning(string message);
}
================================================
FILE: src/MiScaleExporter.MAUI/Services/IScale.cs
================================================
using MiScaleExporter.Models;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace MiScaleExporter.Services
{
public interface IScale
{
BodyComposition BodyComposition { get; set; }
Task GetBodyCompositonAsync(string scaleAddress, User user);
Task CancelSearchAsync();
void StopSearch();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Services/LogService.cs
================================================
using NLog.Config;
using NLog.Targets;
using NLog;
namespace MiScaleExporter.Services;
public class LogService : ILogService
{
private static string TARGET_NAME = "logmemory";
private static string ERRORS_TARGET_NAME = "logmemory_errors";
private static bool _isCreated = false;
private static LoggingConfiguration _configuration;
public LogService()
{
}
public void LogDebug(string message)
{
}
public void LogError(string message)
{
Application.Current.MainPage.DisplayAlert("Error", message,
"OK");
}
public void LogFatal(string message)
{
Application.Current.MainPage.DisplayAlert("Fatal", message,
"OK");
}
public void LogInfo(string message)
{
Application.Current.MainPage.DisplayAlert("Info", message,
"OK");
}
public void LogWarning(string message)
{
Application.Current.MainPage.DisplayAlert("Warning", message,
"OK");
}
public static LoggingConfiguration CreateLogger()
{
if (!_isCreated)
{
_isCreated = true;
LoggingConfiguration configuration = new LoggingConfiguration();
MemoryTarget target = new MemoryTarget(TARGET_NAME);
target.Layout = "${message}";
configuration.AddTarget(target);
LoggingRule traceRule = new LoggingRule("*", NLog.LogLevel.Trace, target);
configuration.LoggingRules.Add(traceRule);
MemoryTarget errorsTarget = new MemoryTarget(ERRORS_TARGET_NAME);
errorsTarget.Layout = "${message}";
configuration.AddTarget(errorsTarget);
LoggingRule errorsRule = new LoggingRule("*", NLog.LogLevel.Error, errorsTarget);
configuration.LoggingRules.Add(errorsRule);
LogManager.Configuration = configuration;
_configuration = configuration;
}
return _configuration;
}
public static IList GetLogs()
{
if (!_isCreated)
{
return new List();
}
var target = LogManager.Configuration.FindTargetByName(TARGET_NAME);
var logEvents = target.Logs;
return logEvents;
}
public static IList GetErrorLogs()
{
if (!_isCreated)
{
return new List();
}
var target = LogManager.Configuration.FindTargetByName(ERRORS_TARGET_NAME);
var logEvents = target.Logs;
return logEvents;
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Services/Scale.cs
================================================
using MiScaleExporter.Models;
using Plugin.BLE;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions.EventArgs;
using System.Reflection;
namespace MiScaleExporter.Services
{
public class Scale : IScale
{
private ILogService _logService;
private IDataInterpreter _dataInterpreter;
private IAdapter _adapter;
private TaskCompletionSource _completionSource;
private BodyComposition _lastSuccessfulBodyComposition;
private byte[] _scannedData;
private string _scaleBlutetoothAddress;
private DateTime? _lastSuccessfulMeasure;
private bool _impedanceWaitFinished = false;
private bool _impedanceWaitStarted = false;
private int _minWeight = 10; // in kilograms
private const double KgToLbsConversion = 2.20462;
private User _user;
private BodyComposition _receivedBodyComposition;
public BodyComposition BodyComposition
{
get { return _receivedBodyComposition; }
set { _receivedBodyComposition = value; }
}
public Scale(ILogService logService, IDataInterpreter dataInterpreter)
{
_logService = logService;
_adapter = CrossBluetoothLE.Current.Adapter;
_adapter.ScanTimeout = 50000;
_adapter.ScanTimeoutElapsed += TimeOuted;
_dataInterpreter = dataInterpreter;
}
public async Task GetBodyCompositonAsync(string scaleAddress, User user)
{
this.BodyComposition = null;
_lastSuccessfulBodyComposition = null;
_receivedBodyComposition = null;
_impedanceWaitFinished = false;
_impedanceWaitStarted = false;
_user = user;
_scaleBlutetoothAddress = scaleAddress;
_completionSource = new TaskCompletionSource();
_adapter.DeviceAdvertised += DeviceAdvertided;
await _adapter.StartScanningForDevicesAsync();
return await _completionSource.Task;
}
private void DeviceAdvertided(object s, DeviceEventArgs a)
{
var obj = a.Device.NativeDevice;
PropertyInfo propInfo = obj.GetType().GetProperty("Address");
string address = (string)propInfo.GetValue(obj, null);
if (address.ToLowerInvariant() == _scaleBlutetoothAddress?.ToLowerInvariant())
{
try
{
var device = a.Device;
var bodyCompositionCandidate = GetScanData(device);
if (bodyCompositionCandidate is not null && bodyCompositionCandidate.Weight > _minWeight)
{
this.BodyComposition = bodyCompositionCandidate;
}
this.ProcessReceivedData();
this.SetPreviews(bodyCompositionCandidate);
}
catch (Exception ex)
{
_logService.LogError(ex.Message);
if (_scannedData != null)
{
_logService.LogInfo(string.Join("; ", _scannedData));
}
}
finally
{
if (this.BodyComposition != null && (this.BodyComposition.HasImpedance || _impedanceWaitFinished))
{
StopAsync().Wait();
_lastSuccessfulMeasure = this.BodyComposition.Date;
this.BodyComposition.IsValid = true;
_completionSource.SetResult(this.BodyComposition);
}
}
}
}
private void ProcessReceivedData()
{
if (this.BodyComposition == null)
{
return;
}
_lastSuccessfulBodyComposition = this.BodyComposition;
if (!this.BodyComposition.IsStabilized)
{
this.BodyComposition = null;
return;
}
else
{
if (_lastSuccessfulMeasure != null && _lastSuccessfulMeasure >= this.BodyComposition.Date)
{
this.BodyComposition = null;
return;
}
if (!_impedanceWaitStarted)
{
_impedanceWaitStarted = true;
Task.Factory.StartNew(async () =>
{
var seconds = 5;
await Task.Delay(TimeSpan.FromSeconds(seconds));
_impedanceWaitStarted = false;
_impedanceWaitFinished = true;
});
}
}
}
public static string BytesToHex(byte[] bytes)
{
return BitConverter.ToString(bytes).Replace("-", "").ToLower();
}
private void SetPreviews(BodyComposition bodyCompositionCandidate)
{
if (Preferences.Get(PreferencesKeys.ShowDebugInfo, false))
{
ScaleMeasurement.Instance.FoundScale = bodyCompositionCandidate != null ? "Connected to scale: Yes" : "Connected to scale: No";
if (bodyCompositionCandidate != null)
{
ScaleMeasurement.Instance.DebugData = (bodyCompositionCandidate.IsStabilized ? "Stabilized: Yes" : "Stabilized: No") + " " + (bodyCompositionCandidate.HasImpedance ? "Impedance: Yes" : "Impedance: No");
if (bodyCompositionCandidate.RawDataLog != null && bodyCompositionCandidate.RawDataLog.Count > 0)
{
ScaleMeasurement.Instance.RawData = string.Join("|", bodyCompositionCandidate.RawDataLog.Select(BytesToHex));
}
else if (bodyCompositionCandidate.ReceivedRawData != null && bodyCompositionCandidate.ReceivedRawData.Length > 0)
{
ScaleMeasurement.Instance.RawData = BytesToHex(bodyCompositionCandidate.ReceivedRawData);
}
}
}
if (this.BodyComposition != null)
{
ScaleMeasurement.Instance.Weight = GetWeightScanningLabel(this.BodyComposition.Weight, Preferences.Get(PreferencesKeys.DisplayWeightInLbs, false));
}
}
private string GetWeightScanningLabel(double valueInKg, bool convertToLbs)
{
return $"{(convertToLbs ? valueInKg * KgToLbsConversion : valueInKg).ToString("0.##")}{(convertToLbs ? "lbs" : "kg")}";
}
private BodyComposition GetScanData(IDevice device)
{
if (device != null)
{
var data = device.AdvertisementRecords
.Where(x => x.Type == Plugin.BLE.Abstractions.AdvertisementRecordType.ServiceData) //0x16
.Select(x => x.Data)
.FirstOrDefault();
_scannedData = data;
var bc = this._dataInterpreter.ComputeData(data, _user, _scaleBlutetoothAddress);
if (bc is not null)
{
bc.ReceivedRawData = _scannedData;
if (!bc.RawDataLog.Contains(_scannedData))
{
bc.RawDataLog.Add(_scannedData);
}
}
return bc;
}
return null;
}
private void CalculateBMIIfEmpty()
{
if (this.BodyComposition is not null && this.BodyComposition.BMI == 0 && _user.Height != 0)
{
var heightInMeters = (double)_user.Height / 100;
this.BodyComposition.BMI = Math.Round(this.BodyComposition.Weight / (heightInMeters * heightInMeters), 2);
}
}
public async Task CancelSearchAsync()
{
try
{
if (this.BodyComposition != null)
{
this.BodyComposition.IsValid = false;
}
if (!_completionSource.Task.IsCompleted)
{
_completionSource.SetResult(this.BodyComposition);
}
}
catch (Exception ex)
{
_logService.LogError(ex.Message);
}
await StopAsync();
}
public void StopSearch()
{
StopAsync().Wait();
if (this.BodyComposition is not null)
{
this.BodyComposition.IsValid = true;
}
else if (_lastSuccessfulBodyComposition is not null)
{
this.BodyComposition = _lastSuccessfulBodyComposition;
this.BodyComposition.IsValid = true;
}
CalculateBMIIfEmpty();
}
private void TimeOuted(object s, EventArgs e)
{
StopAsync().Wait();
_completionSource.SetResult(this.BodyComposition);
}
private async Task StopAsync()
{
await _adapter.StopScanningForDevicesAsync();
_adapter.DeviceAdvertised -= DeviceAdvertided;
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Utils/DoubleValueParser.cs
================================================
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace MiScaleExporter.MAUI.Utils
{
public static class DoubleValueParser
{
public static bool IsValid(string value)
{
if (string.IsNullOrEmpty(value))
{
return true;
}
var parsed = ParseValueFromUsersCulture(value);
return parsed != null;
}
public static string CheckValue(string value)
{
if (IsValid(value))
{
return value;
}
if (value.Length > 1)
{
return CheckValue(value.Remove(value.Length - 1));
}
return string.Empty;
}
public static double? ParseValueFromUsersCulture(string value)
{
if (string.IsNullOrEmpty(value))
{
return null;
}
var parsed = ParseDouble(value);
if (parsed == null)
{
parsed = ParseDouble(value.Replace(',', '.'));
}
if (parsed == null)
{
parsed = ParseDouble(value.Replace('.', ','));
}
if (parsed == null) // when , or . is last character
{
parsed = ParseDouble(value.Remove(value.Length - 1));
}
return parsed;
}
private static double? ParseDouble(string value)
{
if (double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out double parsed))
{
return parsed;
}
else
{
return null;
}
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/ViewModels/AboutViewModel.cs
================================================
using MiScaleExporter.MAUI.Resources.Localization;
using System.Windows.Input;
namespace MiScaleExporter.MAUI.ViewModels;
public class AboutViewModel : BaseViewModel
{
public AboutViewModel()
{
this.Title = AppSnippets.MiScaleExporter;
GoToScanCommand = new Command(async () =>
await Shell.Current.GoToAsync("///ScalePage"));
OpenGithubCommand = new Command(async () =>
await Browser.OpenAsync("https://github.com/lswiderski/mi-scale-exporter"));
OpenCoffeeCommand = new Command(async () =>
await Browser.OpenAsync("https://www.buymeacoffee.com/lukaszswiderski"));
OpenHelpCommand = new Command(async () =>
await Shell.Current.GoToAsync("///HelpPage"));
}
public ICommand GoToScanCommand { get; }
public ICommand OpenGithubCommand { get; }
public ICommand OpenCoffeeCommand { get; }
public ICommand OpenHelpCommand { get; }
}
================================================
FILE: src/MiScaleExporter.MAUI/ViewModels/BaseViewModel.cs
================================================
using MiScaleExporter.Models;
using MiScaleExporter.Services;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MiScaleExporter.MAUI.ViewModels
{
public class BaseViewModel : INotifyPropertyChanged
{
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { SetProperty(ref isBusy, value); }
}
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
protected bool SetProperty(ref T backingStore, T value,
[CallerMemberName] string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
protected void NotifyAllPropertiesChanged()
{
PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(null));
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/ViewModels/FormViewModel.cs
================================================
using CommunityToolkit.Maui.Alerts;
using CommunityToolkit.Maui.Storage;
using MiScaleExporter.MAUI;
using MiScaleExporter.MAUI.Resources.Localization;
using MiScaleExporter.MAUI.Utils;
using MiScaleExporter.Models;
using MiScaleExporter.Services;
using System.Globalization;
using System.Threading;
using YetAnotherGarminConnectClient.Dto.Garmin.Fit;
namespace MiScaleExporter.MAUI.ViewModels
{
public class FormViewModel : BaseViewModel, IFormViewModel
{
private readonly IGarminService _garminService;
private readonly IFileSaver _fileSaver;
private const double KgToLbsConversion = 2.20462;
public FormViewModel(IGarminService garminService, IFileSaver fileSaver)
{
_garminService = garminService;
Title = AppSnippets.GarminBodyCompositionForm;
Date = DateTime.Now;
Time = DateTime.Now.TimeOfDay;
_muscleMassAsKg = true;
_showWeightInKg = true;
UploadCommand = new Command(OnUpload, ValidateSave);
GenerateFitFileCommand = new Command(OnGenerateFitFileAsync);
CancelMFACommand = new Command(OnCancelMFA);
this.PropertyChanged +=
(_, __) => UploadCommand.ChangeCanExecute();
this._fileSaver = fileSaver;
}
public async Task LoadPreferencesAsync()
{
this._email = Preferences.Get(PreferencesKeys.GarminUserEmail, string.Empty);
this._password = await SecureStorage.GetAsync(PreferencesKeys.GarminUserPassword);
this._accessToken = await SecureStorage.GetAsync(PreferencesKeys.GarminUserAccessToken);
this._tokenSecret = await SecureStorage.GetAsync(PreferencesKeys.GarminUserTokenSecret);
this._saveTokens = !string.IsNullOrWhiteSpace(_email) && !string.IsNullOrWhiteSpace(_password);
this.ShowEmail = string.IsNullOrWhiteSpace(_email);
this.ShowPassword = string.IsNullOrWhiteSpace(_password);
this._displayWeightInLbs = Preferences.Get(PreferencesKeys.DisplayWeightInLbs, false);
this.ShowWeightInKg = !_displayWeightInLbs;
this.ShowWeightInLbs = _displayWeightInLbs;
this.MuscleMassAsPercentage = Preferences.Get(PreferencesKeys.MuscleMassAsPercentage, false);
this.MuscleMassAsKg = (!MuscleMassAsPercentage && ShowWeightInKg);
this.MuscleMassAsLbs = (!MuscleMassAsPercentage && !ShowWeightInKg);
this.Date = DateTime.Now;
this.Time = DateTime.Now.TimeOfDay;
}
private bool ValidateSave()
{
return !String.IsNullOrWhiteSpace(_email)
&& !String.IsNullOrWhiteSpace(_password);
}
public void AutoUpload()
{
if (!string.IsNullOrWhiteSpace(_email)
&& !string.IsNullOrWhiteSpace(_password))
{
OnUpload();
}
}
private async void OnUpload()
{
this.IsBusyForm = true;
var credencials = new CredentialsData
{
Email = _email,
Password = _password,
AccessToken = this._accessToken,
TokenSecret = this._tokenSecret,
};
var response = await this._garminService.UploadAsync(this.PrepareRequest(), Date.Date.Add(Time), credencials);
var message = (response?.IsSuccess ?? false) ? AppSnippets.Uploaded : response?.Message;
await Application.Current.MainPage.DisplayAlert(AppSnippets.Response, message, AppSnippets.OK);
this.IsBusyForm = false;
if (this._saveTokens)
{
this._accessToken = response?.AccessToken ?? string.Empty;
this._tokenSecret = response?.TokenSecret ?? string.Empty;
}
else
{
this._accessToken = string.Empty;
this._tokenSecret = string.Empty;
}
await SecureStorage.SetAsync(PreferencesKeys.GarminUserAccessToken, this._accessToken);
await SecureStorage.SetAsync(PreferencesKeys.GarminUserTokenSecret, this._tokenSecret);
if (response?.MFARequested ?? false)
{
this.ShowMFACode = true;
this.ShowEmail = false;
this.ShowPassword = false;
this.ExternalApiClientId = response?.ExternalApiClientId;
}
else
{
this.ShowMFACode = false;
this.MFACode = null;
this.ExternalApiClientId = null;
this.ShowEmail = string.IsNullOrWhiteSpace(Preferences.Get(PreferencesKeys.GarminUserEmail, string.Empty));
this.ShowPassword = string.IsNullOrWhiteSpace(await SecureStorage.GetAsync(PreferencesKeys.GarminUserPassword));
}
// This will pop the current page off the navigation stack
await Shell.Current.GoToAsync("..?autoUpload=false");
}
private async void OnGenerateFitFileAsync()
{
this.IsBusyForm = true;
var response = await this._garminService.GenerateFitFileAsync(this.PrepareRequest(), Date.Date.Add(Time));
if (!response.IsSuccess && response.file != null)
{
await Application.Current.MainPage.DisplayAlert(AppSnippets.Response, response?.Message, AppSnippets.OK);
}
else
{
using var stream = new MemoryStream(response.file);
var fileSaverResult = await _fileSaver.SaveAsync($"activity_{Date.Date.Add(Time).ToShortDateString()}.fit", stream);
if (fileSaverResult.IsSuccessful)
{
await Toast.Make($"The file was saved successfully to location: {fileSaverResult.FilePath}").Show();
}
else
{
await Toast.Make($"The file was not saved successfully with error: {fileSaverResult.Exception.Message}").Show();
}
}
this.IsBusyForm = false;
// This will pop the current page off the navigation stack
await Shell.Current.GoToAsync("..?autoUpload=false");
}
private async void OnCancelMFA()
{
this.ShowMFACode = false;
this.MFACode = null;
this.ExternalApiClientId = null;
this.ShowEmail = string.IsNullOrWhiteSpace(Preferences.Get(PreferencesKeys.GarminUserEmail, string.Empty));
this.ShowPassword = string.IsNullOrWhiteSpace(await SecureStorage.GetAsync(PreferencesKeys.GarminUserPassword));
}
private BodyComposition PrepareRequest()
{
var bc = new BodyComposition
{
Fat = DoubleValueParser.ParseValueFromUsersCulture(_fat) ?? 0,
BodyType = _bodyType ?? 0,
Weight = ConvertToKg(DoubleValueParser.ParseValueFromUsersCulture(_weight) ?? 0),
BoneMass = ConvertToKg(DoubleValueParser.ParseValueFromUsersCulture(_boneMass) ?? 0),
MuscleMass = ConvertToKg(DoubleValueParser.ParseValueFromUsersCulture(_muscleMass) ?? 0),
MetabolicAge = DoubleValueParser.ParseValueFromUsersCulture(_metabolicAge) ?? 0,
ProteinPercentage = DoubleValueParser.ParseValueFromUsersCulture(_proteinPercentage) ?? 0,
VisceralFat = DoubleValueParser.ParseValueFromUsersCulture(_visceralFat) ?? 0,
BMI = DoubleValueParser.ParseValueFromUsersCulture(_bmi) ?? 0,
BMR = DoubleValueParser.ParseValueFromUsersCulture(_bmr) ?? 0,
WaterPercentage = DoubleValueParser.ParseValueFromUsersCulture(_waterPercentage) ?? 0,
MFACode = _mfaCode,
ExternalApiClientId = _externalApiClientId
};
if (Preferences.Get(PreferencesKeys.MuscleMassAsPercentage, false)
&& bc.MuscleMass != 0
&& bc.Weight != 0)
{
bc.MuscleMass = (bc.MuscleMass / 100) * bc.Weight;
}
return bc;
}
public void LoadBodyComposition()
{
if (App.BodyComposition is null) return;
Weight = ConvertFromKg(App.BodyComposition.Weight).ToString("0.##");
BMI = App.BodyComposition.BMI.ToString();
BoneMass = ConvertFromKg(App.BodyComposition.BoneMass).ToString("0.##");
MuscleMass = ConvertFromKg(App.BodyComposition.MuscleMass).ToString("0.##");
IdealWeight = ConvertFromKg(App.BodyComposition.IdealWeight).ToString("0.##");
BMR = App.BodyComposition.BMR.ToString();
MetabolicAge = App.BodyComposition.MetabolicAge.ToString();
ProteinPercentage = App.BodyComposition.ProteinPercentage.ToString();
VisceralFat = App.BodyComposition.VisceralFat.ToString();
Fat = App.BodyComposition.Fat.ToString();
WaterPercentage = App.BodyComposition.WaterPercentage.ToString();
BodyType = App.BodyComposition.BodyType;
IsAutomaticCalculation = true;
}
public Command UploadCommand { get; }
public Command CancelMFACommand { get; }
public Command GenerateFitFileCommand { get; }
private string _weight;
public string Weight
{
get => _weight;
set => SetProperty(ref _weight, DoubleValueParser.CheckValue(value));
}
private string _bmi;
public string BMI
{
get => _bmi;
set => SetProperty(ref _bmi, DoubleValueParser.CheckValue(value));
}
private string _idealWeight;
public string IdealWeight
{
get => _idealWeight;
set => SetProperty(ref _idealWeight, DoubleValueParser.CheckValue(value));
}
private string _metabolicAge;
public string MetabolicAge
{
get => _metabolicAge;
set => SetProperty(ref _metabolicAge, DoubleValueParser.CheckValue(value));
}
private string _proteinPercentage;
public string ProteinPercentage
{
get => _proteinPercentage;
set => SetProperty(ref _proteinPercentage, DoubleValueParser.CheckValue(value));
}
private string _bmr;
public string BMR
{
get => _bmr;
set => SetProperty(ref _bmr, DoubleValueParser.CheckValue(value));
}
private string _fat;
public string Fat
{
get => _fat;
set => SetProperty(ref _fat, DoubleValueParser.CheckValue(value));
}
private string _muscleMass;
public string MuscleMass
{
get => _muscleMass;
set => SetProperty(ref _muscleMass, DoubleValueParser.CheckValue(value));
}
private string _boneMass;
public string BoneMass
{
get => _boneMass;
set => SetProperty(ref _boneMass, DoubleValueParser.CheckValue(value));
}
private string _visceralFat;
public string VisceralFat
{
get => _visceralFat;
set => SetProperty(ref _visceralFat, DoubleValueParser.CheckValue(value));
}
private int? _bodyType;
public int? BodyType
{
get => _bodyType;
set => SetProperty(ref _bodyType, value);
}
private string _waterPercentage;
public string WaterPercentage
{
get => _waterPercentage;
set => SetProperty(ref _waterPercentage, DoubleValueParser.CheckValue(value));
}
private string _email;
private string _password;
private string _accessToken;
private string _tokenSecret;
private bool _saveTokens;
private DateTime _date;
public DateTime Date
{
get => _date;
set => SetProperty(ref _date, value);
}
private TimeSpan _time;
public TimeSpan Time
{
get => _time;
set => SetProperty(ref _time, value);
}
private bool _isAutomaticCalculation;
public bool IsAutomaticCalculation
{
get => _isAutomaticCalculation;
set => SetProperty(ref _isAutomaticCalculation, value);
}
private bool _isBusyForm;
public bool IsBusyForm
{
get => _isBusyForm;
set => SetProperty(ref _isBusyForm, value);
}
private bool _showMFACode;
public bool ShowMFACode
{
get => _showMFACode;
set => SetProperty(ref _showMFACode, value);
}
private string _externalApiClientId;
public string ExternalApiClientId
{
get => _externalApiClientId;
set
{
SetProperty(ref _externalApiClientId, value);
}
}
private string _mfaCode;
public string MFACode
{
get => _mfaCode;
set
{
SetProperty(ref _mfaCode, value);
}
}
private bool _muscleMassAsPercentage;
public bool MuscleMassAsPercentage
{
get => _muscleMassAsPercentage;
set => SetProperty(ref _muscleMassAsPercentage, value);
}
private bool _muscleMassAsKg;
public bool MuscleMassAsKg
{
get => _muscleMassAsKg;
set => SetProperty(ref _muscleMassAsKg, value);
}
private bool _muscleMassAsLbs;
public bool MuscleMassAsLbs
{
get => _muscleMassAsLbs;
set => SetProperty(ref _muscleMassAsLbs, value);
}
private bool _showWeightInKg;
public bool ShowWeightInKg
{
get => _showWeightInKg;
set => SetProperty(ref _showWeightInKg, value);
}
private bool _showWeightInLbs;
public bool ShowWeightInLbs
{
get => _showWeightInLbs;
set => SetProperty(ref _showWeightInLbs, value);
}
private double ConvertFromKg(double valueInKg)
{
return _displayWeightInLbs ? valueInKg * KgToLbsConversion : valueInKg;
}
private double ConvertToKg(double displayValue)
{
return _displayWeightInLbs ? displayValue / KgToLbsConversion : displayValue;
}
private bool _displayWeightInLbs;
public string Email
{
get => _email;
set
{
SetProperty(ref _email, value);
UploadCommand?.ChangeCanExecute();
}
}
public string Password
{
get => _password;
set
{
SetProperty(ref _password, value);
UploadCommand?.ChangeCanExecute();
}
}
private bool _showEmail;
private bool _showPassword;
public bool ShowEmail
{
get => _showEmail;
set => SetProperty(ref _showEmail, value);
}
public bool ShowPassword
{
get => _showPassword;
set => SetProperty(ref _showPassword, value);
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/ViewModels/IFormViewModel.cs
================================================
namespace MiScaleExporter.MAUI.ViewModels
{
public interface IFormViewModel
{
void LoadBodyComposition();
Task LoadPreferencesAsync();
void AutoUpload();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/ViewModels/IScaleViewModel.cs
================================================
using System;
using System.Collections.Generic;
using System.Text;
namespace MiScaleExporter.MAUI.ViewModels
{
public interface IScaleViewModel
{
Task CheckPreferencesAsync();
Task LoadPreferencesAsync();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/ViewModels/ISettingsViewModel.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MiScaleExporter.MAUI.ViewModels
{
public interface ISettingsViewModel
{
void SexRadioSetToMale();
void SexRadioSetToFemale();
void ScaleTypeSetToBodyCompositionScale();
void ScaleTypeSetToMiscale();
void ScaleTypeSetToS400();
Task LoadPreferencesAsync();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/ViewModels/ScaleViewModel.cs
================================================
using MiScaleExporter.Models;
using MiScaleExporter.Services;
using MiScaleExporter.Permission;
using MiScaleExporter.MAUI.Resources.Localization;
using Plugin.BLE;
using Plugin.BLE.Abstractions;
namespace MiScaleExporter.MAUI.ViewModels
{
public class ScaleViewModel : BaseViewModel, IScaleViewModel
{
private readonly IScale _scale;
private readonly ILogService _logService;
private string _address;
private int _age;
private int _height;
private Models.Sex _sex;
private ScaleType _scaleType;
private string _bindkey;
public ScaleViewModel(IScale scale, ILogService logService)
{
_scale = scale;
_logService = logService;
Title = AppSnippets.MiScaleData;
CancelCommand = new Command(OnCancel);
StopCommand = new Command(OnStop);
}
public async Task CheckPreferencesAsync()
{
ScaleMeasurement.Instance.Weight = "";
App.BodyComposition = null;
var hasPermissions = await CheckPermissions();
if(hasPermissions)
{
await this.LoadPreferencesAsync();
if (!string.IsNullOrWhiteSpace(_address))
{
OnScan();
}
else
{
// await App.Current.MainPage.Navigation.PopAsync();
await Shell.Current.GoToAsync($"//Settings");
}
}
}
public async Task LoadPreferencesAsync()
{
this._age = Preferences.Get(PreferencesKeys.UserAge, 25);
this._height = Preferences.Get(PreferencesKeys.UserHeight, 170);
this._sex = (Models.Sex)Preferences.Get(PreferencesKeys.UserSex, (byte)Models.Sex.Male);
this._address = Preferences.Get(PreferencesKeys.MiScaleBluetoothAddress, string.Empty);
this._scaleType = (ScaleType)Preferences.Get(PreferencesKeys.ScaleType, (byte)ScaleType.MiBodyCompositionScale);
this._bindkey = Preferences.Get(PreferencesKeys.S400Bindkey, string.Empty);
}
private async void OnScan()
{
await StartScan();
}
private async Task CheckPermissions()
{
if (DeviceInfo.Platform == DevicePlatform.Android)
{
if (DeviceInfo.Version.Major >= 12)
{
if (await GetBluetoothPermissionStatusAsync() != PermissionStatus.Granted)
{
await Application.Current.MainPage.DisplayAlert(AppSnippets.Problem, AppSnippets.PermissionBluetoothRequired,
AppSnippets.OK);
return false;
}
if (await GetLocationWhenInUsePermissionStatusAsync() != PermissionStatus.Granted)
{
await Application.Current.MainPage.DisplayAlert(AppSnippets.Problem, AppSnippets.PermissionLocationRequired,
AppSnippets.OK);
return false;
}
}
else
{
if (await GetLocationWhenInUsePermissionStatusAsync() != PermissionStatus.Granted)
{
if(await GetLocationAlwaysPermissionStatusAsync() != PermissionStatus.Granted)
{
await Application.Current.MainPage.DisplayAlert(AppSnippets.Problem, AppSnippets.PermissionLocationRequired,
AppSnippets.OK);
return false;
}
}
}
}
// Check if Bluetooth is enabled
if (CrossBluetoothLE.Current.State != BluetoothState.On)
{
await Application.Current.MainPage.DisplayAlert(
AppSnippets.Problem,
AppSnippets.BluetoothDisabled,
AppSnippets.OK);
return false;
}
return true;
}
private async Task StartScan()
{
ScanningLabel = string.Empty;
ScaleMeasurement.Instance.Weight = "0";
this.IsBusyForm = true;
await this._scale.GetBodyCompositonAsync(_address,
new User { Sex = _sex, Age = _age, Height = _height, ScaleType = _scaleType, BindKey = _bindkey });
this.OnStop();
}
private async void OnStop()
{
this._scale.StopSearch();
this.IsBusyForm = false;
if (this._scale.BodyComposition is null || !this._scale.BodyComposition.IsValid)
{
var msg = AppSnippets.DataCouldNotBeObtained;
await Application.Current.MainPage.DisplayAlert(AppSnippets.Problem, msg,
AppSnippets.OK);
_logService.LogError(msg);
ScanningLabel = AppSnippets.NotFound;
}
else
{
App.BodyComposition = this._scale.BodyComposition;
await Shell.Current.GoToAsync($"//FormPage?autoUpload={Preferences.Get(PreferencesKeys.OneClickScanAndUpload, false)}");
}
}
private async void OnCancel()
{
await this._scale.CancelSearchAsync();
this.IsBusyForm = false;
}
private async Task GetLocationWhenInUsePermissionStatusAsync()
{
var locationPermissionStatus = await Permissions.CheckStatusAsync();
if (locationPermissionStatus != PermissionStatus.Granted)
{
locationPermissionStatus = await Permissions.RequestAsync();
}
return locationPermissionStatus;
}
private async Task GetLocationAlwaysPermissionStatusAsync()
{
var locationPermissionStatus = await Permissions.CheckStatusAsync();
if (locationPermissionStatus != PermissionStatus.Granted)
{
locationPermissionStatus = await Permissions.RequestAsync();
}
return locationPermissionStatus;
}
private async Task GetBluetoothPermissionStatusAsync()
{
var bluetoothPermission = DependencyService.Get();
var status = await bluetoothPermission.CheckStatusAsync();
if (status != PermissionStatus.Granted)
{
status = await bluetoothPermission.RequestAsync();
}
return status;
}
public Command CancelCommand { get; }
public Command StopCommand { get; }
private string _scanningLabel;
public string ScanningLabel
{
get => _scanningLabel;
set => SetProperty(ref _scanningLabel, value);
}
private bool _isBusyForm;
public bool IsBusyForm
{
get => _isBusyForm;
set => SetProperty(ref _isBusyForm, value);
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/ViewModels/SettingsViewModel.cs
================================================
using System;
using System.Windows.Input;
using MiScaleExporter.MAUI.Resources.Localization;
using MiScaleExporter.Models;
namespace MiScaleExporter.MAUI.ViewModels
{
public class SettingsViewModel : BaseViewModel, ISettingsViewModel
{
public SettingsViewModel()
{
this.Title = AppSnippets.Settings;
// Initialize birthDate with a reasonable default to avoid binding issues
this._birthDate = DateTime.Today.AddYears(-25);
ResetCommand = new Command(() =>
{
Preferences.Remove(PreferencesKeys.ApiServerAddressOverride);
this.ApiAddress = string.Empty;
Preferences.Remove(PreferencesKeys.OneClickScanAndUpload);
Preferences.Remove(PreferencesKeys.UseExternalAPI);
Preferences.Remove(PreferencesKeys.ShowDebugInfo);
Preferences.Remove(PreferencesKeys.HideAds);
Preferences.Remove(PreferencesKeys.MuscleMassAsPercentage);
Preferences.Remove(PreferencesKeys.DisplayWeightInLbs);
Preferences.Remove(PreferencesKeys.UseChinaServer);
Preferences.Remove(PreferencesKeys.UserAge);
Preferences.Remove(PreferencesKeys.UserBirthDate);
Preferences.Remove(PreferencesKeys.UseBirthDateMode);
this.UseBirthDateMode = false;
this.ManualAge = "25";
}
);
GetBLEKeyCommand = new Command(async () => await Launcher.OpenAsync("https://lswiderski.github.io/mi-scale-exporter/#steps-to-connect-xiaomi-body-composition-scale-s400"));
ResetTokensCommand = new Command(_clearTokens);
}
public ICommand ResetCommand { get; }
public ICommand GetBLEKeyCommand { get; }
public ICommand ResetTokensCommand { get; }
public async Task LoadPreferencesAsync()
{
_isLoadingPreferences = true;
try
{
this._apiAddress = Preferences.Get(PreferencesKeys.ApiServerAddressOverride, string.Empty);
this._oneClickScanAndUpload = Preferences.Get(PreferencesKeys.OneClickScanAndUpload, false);
this._useExternalAPI = Preferences.Get(PreferencesKeys.UseExternalAPI, false);
this._showDebugInfo = Preferences.Get(PreferencesKeys.ShowDebugInfo, false);
this._hideAds = Preferences.Get(PreferencesKeys.HideAds, false);
this._muscleMassAsPercentage = Preferences.Get(PreferencesKeys.MuscleMassAsPercentage, false);
this._displayWeightInLbs = Preferences.Get(PreferencesKeys.DisplayWeightInLbs, false);
this._useChinaServer = Preferences.Get(PreferencesKeys.UseChinaServer, false);
// Load age or birthday mode
this._useBirthDateMode = Preferences.Get(PreferencesKeys.UseBirthDateMode, false);
if (_useBirthDateMode)
{
// Load birthday
var birthDateTicks = Preferences.Get(PreferencesKeys.UserBirthDate, 0L);
this._birthDate = birthDateTicks > 0 ? new DateTime(birthDateTicks) : DateTime.Today.AddYears(-25);
this._manualAge = 0; // Not used in birthday mode
}
else
{
// Load manual age
this._manualAge = Preferences.Get(PreferencesKeys.UserAge, 25);
this._birthDate = DateTime.Today.AddYears(-_manualAge); // For reference only
}
this._height = Preferences.Get(PreferencesKeys.UserHeight, 170);
this._sex = (Sex)Preferences.Get(PreferencesKeys.UserSex, (byte)Sex.Male);
this._address = Preferences.Get(PreferencesKeys.MiScaleBluetoothAddress, string.Empty);
this._scaleType = (ScaleType)Preferences.Get(PreferencesKeys.ScaleType, (byte)ScaleType.MiBodyCompositionScale);
this._email = Preferences.Get(PreferencesKeys.GarminUserEmail, string.Empty);
this._password = await SecureStorage.GetAsync(PreferencesKeys.GarminUserPassword);
this._bindkey = Preferences.Get(PreferencesKeys.S400Bindkey, string.Empty);
NotifyAllPropertiesChanged();
}
finally
{
_isLoadingPreferences = false;
}
}
private bool ValidateProfile()
{
return !String.IsNullOrWhiteSpace(_address)
&& _height > 0 && _height < 220
&& Age > 0 && Age < 99;
}
private string _apiAddress;
public string ApiAddress
{
get => _apiAddress;
set
{
if (string.IsNullOrEmpty(value))
{
Preferences.Remove(PreferencesKeys.ApiServerAddressOverride);
}
else if (Uri.TryCreate(value, UriKind.Absolute, out var uri))
{
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
{
Preferences.Set(PreferencesKeys.ApiServerAddressOverride, value);
}
}
SetProperty(ref _apiAddress, value);
}
}
private bool _oneClickScanAndUpload;
public bool OneClickScanAndUpload
{
get => _oneClickScanAndUpload;
set
{
Preferences.Set(PreferencesKeys.OneClickScanAndUpload, value);
SetProperty(ref _oneClickScanAndUpload, value);
}
}
private bool _useExternalAPI;
public bool UseExternalAPI
{
get => _useExternalAPI;
set
{
Preferences.Set(PreferencesKeys.UseExternalAPI, value);
SetProperty(ref _useExternalAPI, value);
}
}
private bool _muscleMassAsPercentage;
public bool MuscleMassAsPercentage
{
get => _muscleMassAsPercentage;
set
{
Preferences.Set(PreferencesKeys.MuscleMassAsPercentage, value);
SetProperty(ref _muscleMassAsPercentage, value);
}
}
private bool _displayWeightInLbs;
public bool DisplayWeightInLbs
{
get => _displayWeightInLbs;
set
{
Preferences.Set(PreferencesKeys.DisplayWeightInLbs, value);
SetProperty(ref _displayWeightInLbs, value);
}
}
private bool _useChinaServer;
public bool UseChinaServer
{
get => _useChinaServer;
set
{
Preferences.Set(PreferencesKeys.UseChinaServer, value);
SetProperty(ref _useChinaServer, value);
}
}
private bool _showDebugInfo;
public bool ShowDebugInfo
{
get => _showDebugInfo;
set
{
Preferences.Set(PreferencesKeys.ShowDebugInfo, value);
SetProperty(ref _showDebugInfo, value);
}
}
private bool _hideAds;
public bool HideAds
{
get => _hideAds;
set
{
Preferences.Set(PreferencesKeys.HideAds, value);
SetProperty(ref _hideAds, value);
}
}
public void SexRadioSetToMale()
{
this.Sex = Sex.Male;
}
public void SexRadioSetToFemale()
{
this.Sex = Sex.Female;
}
public void ScaleTypeSetToBodyCompositionScale()
{
this.ScaleType = ScaleType.MiBodyCompositionScale;
}
public void ScaleTypeSetToMiscale()
{
this.ScaleType = ScaleType.MiSmartScale;
}
public void ScaleTypeSetToS400()
{
this.ScaleType = ScaleType.S400;
}
public void CheckPreferences()
{
}
private string _address;
public string Address
{
get => _address;
set
{
SetProperty(ref _address, value);
Preferences.Set(PreferencesKeys.MiScaleBluetoothAddress, value);
}
}
private string _bindkey;
public string Bindkey
{
get => _bindkey;
set
{
SetProperty(ref _bindkey, value);
Preferences.Set(PreferencesKeys.S400Bindkey, value);
}
}
private int _age;
public int Age
{
get
{
if (_useBirthDateMode)
{
// Calculate age from birthday
var today = DateTime.Today;
var age = today.Year - _birthDate.Year;
if (_birthDate.Date > today.AddYears(-age))
{
age--;
}
return age;
}
else
{
// Return manually entered age
return _manualAge;
}
}
}
private int _manualAge;
public string ManualAge
{
get => _manualAge.ToString();
set
{
if (value is null) return;
if (int.TryParse(value, out var result))
{
SetProperty(ref _manualAge, result);
if (result == 0) return;
Preferences.Set(PreferencesKeys.UserAge, result);
OnPropertyChanged(nameof(Age));
}
}
}
private bool _useBirthDateMode;
public bool UseBirthDateMode
{
get => _useBirthDateMode;
set
{
if (SetProperty(ref _useBirthDateMode, value))
{
Preferences.Set(PreferencesKeys.UseBirthDateMode, value);
OnPropertyChanged(nameof(Age));
}
}
}
private DateTime _birthDate;
private bool _isLoadingPreferences = false;
public DateTime BirthDate
{
get => _birthDate;
set
{
if (SetProperty(ref _birthDate, value))
{
// Only save to preferences if we're not in the middle of loading
if (!_isLoadingPreferences)
{
Preferences.Set(PreferencesKeys.UserBirthDate, value.Ticks);
OnPropertyChanged(nameof(Age));
}
}
}
}
private int _height;
public string Height
{
get => _height.ToString();
set
{
if (value is null) return;
if (int.TryParse(value, out var result))
{
SetProperty(ref _height, result);
if (result == 0) return;
Preferences.Set(PreferencesKeys.UserHeight, result);
}
}
}
public bool IsMaleSelected
{
get => _sex == Sex.Male;
}
public bool IsFemaleSelected
{
get => _sex == Sex.Female;
}
private Sex _sex;
public Sex Sex
{
get => _sex;
set
{
if (_sex == value) return;
SetProperty(ref _sex, value);
Preferences.Set(PreferencesKeys.UserSex, (byte)value);
}
}
public bool IsMiBodyCompositionScaleSelected
{
get => _scaleType == ScaleType.MiBodyCompositionScale;
}
public bool IsMiSmartScaleSelected
{
get => _scaleType == ScaleType.MiSmartScale;
}
public bool IsS400Selected
{
get => _scaleType == ScaleType.S400;
}
private ScaleType _scaleType;
public ScaleType ScaleType
{
get => _scaleType;
set
{
if (_scaleType != value)
{
SetProperty(ref _scaleType, value);
Preferences.Set(PreferencesKeys.ScaleType, (byte)value);
}
}
}
private string _email;
public string Email
{
get => _email;
set
{if(_email != value)
{
SetProperty(ref _email, value);
Preferences.Set(PreferencesKeys.GarminUserEmail, value);
this._clearTokens();
}
}
}
private string _password;
public string Password
{
get => _password;
set
{
if(_password != value)
{
SetProperty(ref _password, value);
SecureStorage.SetAsync(PreferencesKeys.GarminUserPassword, value);
this._clearTokens();
}
}
}
private void _clearTokens()
{
SecureStorage.SetAsync(PreferencesKeys.GarminUserAccessToken, string.Empty);
SecureStorage.SetAsync(PreferencesKeys.GarminUserTokenSecret, string.Empty);
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Views/AboutPage.xaml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/Views/AboutPage.xaml.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Autofac;
using MiScaleExporter.MAUI;
using MiScaleExporter.MAUI.ViewModels;
using MiScaleExporter.Models;
namespace MiScaleExporter.MAUI.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class AboutPage : ContentPage
{
public AboutPage()
{
InitializeComponent();
using (var scope = App.Container.BeginLifetimeScope())
{
this.BindingContext = scope.Resolve();
}
}
protected override void OnAppearing()
{
this.adMobBanner.IsVisible = !Preferences.Get(PreferencesKeys.HideAds, false);
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Views/FormPage.xaml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/Views/FormPage.xaml.cs
================================================
using Autofac;
using MiScaleExporter.MAUI.ViewModels;
using MiScaleExporter.Models;
namespace MiScaleExporter.MAUI.Views
{
[QueryProperty(nameof(AutoUpload), "autoUpload")]
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class FormPage : ContentPage
{
private IFormViewModel vm;
public FormPage()
{
InitializeComponent();
using (var scope = App.Container.BeginLifetimeScope())
{
this.BindingContext = vm = scope.Resolve();
}
}
protected override async void OnAppearing()
{
base.OnAppearing();
this.adMobBanner.IsVisible = !Preferences.Get(PreferencesKeys.HideAds, false);
await vm.LoadPreferencesAsync();
vm.LoadBodyComposition();
}
public bool AutoUpload
{
set
{
if (value)
{
vm.AutoUpload();
}
}
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Views/HelpPage.xaml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/Views/HelpPage.xaml.cs
================================================
using System.Windows.Input;
namespace MiScaleExporter.MAUI.Views;
public partial class HelpPage : ContentPage
{
public ICommand OpenGithubCommand { get; }
public ICommand OpenCoffeeCommand { get; }
public ICommand S400HelpCommand { get; } = new Command(async () =>
await Launcher.OpenAsync("https://lswiderski.github.io/mi-scale-exporter/#steps-to-connect-xiaomi-body-composition-scale-s400"));
public HelpPage()
{
this.BindingContext = this;
OpenGithubCommand = new Command(async () =>
await Browser.OpenAsync("https://github.com/lswiderski/mi-scale-exporter"));
OpenCoffeeCommand = new Command(async () =>
await Browser.OpenAsync("https://www.buymeacoffee.com/lukaszswiderski"));
InitializeComponent();
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Views/ScalePage.xaml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/Views/ScalePage.xaml.cs
================================================
using Autofac;
using MiScaleExporter.Models;
using MiScaleExporter.MAUI.ViewModels;
namespace MiScaleExporter.MAUI.Views
{
public partial class ScalePage : ContentPage
{
private IScaleViewModel vm;
public ScalePage()
{
InitializeComponent();
using (var scope = App.Container.BeginLifetimeScope())
{
this.BindingContext = vm = scope.Resolve();
}
}
protected override async void OnAppearing()
{
base.OnAppearing();
this.adMobBanner.IsVisible = !Preferences.Get(PreferencesKeys.HideAds, false);
await vm.CheckPreferencesAsync();
}
}
}
================================================
FILE: src/MiScaleExporter.MAUI/Views/SettingsPage.xaml
================================================
================================================
FILE: src/MiScaleExporter.MAUI/Views/SettingsPage.xaml.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Autofac;
using MiScaleExporter.MAUI;
using MiScaleExporter.MAUI.ViewModels;
namespace MiScaleExporter.MAUI.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class SettingPage : ContentPage
{
private ISettingsViewModel vm;
public SettingPage()
{
InitializeComponent();
using (var scope = App.Container.BeginLifetimeScope())
{
this.BindingContext = vm = scope.Resolve();
}
}
private void SexRadioSetToMale(object sender, CheckedChangedEventArgs e)
{
if (e.Value)
{
vm.SexRadioSetToMale();
}
}
private void SexRadioSetToFemale(object sender, CheckedChangedEventArgs e)
{
if (e.Value)
{
vm.SexRadioSetToFemale();
}
}
private void ScaleTypeSetToBodyCompositionScale(object sender, CheckedChangedEventArgs e)
{
if (e.Value)
{
vm.ScaleTypeSetToBodyCompositionScale();
}
}
private void ScaleTypeSetToMiscale(object sender, CheckedChangedEventArgs e)
{
if (e.Value)
{
vm.ScaleTypeSetToMiscale();
}
}
private void ScaleTypeSetToS400(object sender, CheckedChangedEventArgs e)
{
if (e.Value)
{
vm.ScaleTypeSetToS400();
}
}
protected override async void OnAppearing()
{
base.OnAppearing();
await vm.LoadPreferencesAsync();
}
}
}