├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE
├── POLICY
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── compose_stability.conf
├── proguard-rules.pro
├── schemas
│ └── com.xinto.mauth.db.AccountDatabase
│ │ ├── 1.json
│ │ ├── 2.json
│ │ ├── 3.json
│ │ ├── 4.json
│ │ └── 5.json
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── xinto
│ │ └── mauth
│ │ ├── ExampleInstrumentedTest.kt
│ │ └── OtpStringTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ └── com
│ │ └── xinto
│ │ └── mauth
│ │ ├── Mauth.kt
│ │ ├── core
│ │ ├── auth
│ │ │ ├── AuthManager.kt
│ │ │ └── DefaultAuthManager.kt
│ │ ├── camera
│ │ │ ├── QrCodeAnalyzer.kt
│ │ │ ├── ZxingDecoder.kt
│ │ │ └── ZxingEncoder.kt
│ │ ├── otp
│ │ │ ├── exporter
│ │ │ │ ├── DefaultOtpExporter.kt
│ │ │ │ └── OtpExporter.kt
│ │ │ ├── generator
│ │ │ │ ├── DefaultOtpGenerator.kt
│ │ │ │ └── OtpGenerator.kt
│ │ │ ├── model
│ │ │ │ ├── OtpData.kt
│ │ │ │ ├── OtpDigest.kt
│ │ │ │ └── OtpType.kt
│ │ │ ├── parser
│ │ │ │ ├── DefaultOtpUriParser.kt
│ │ │ │ ├── OtpUriParser.kt
│ │ │ │ ├── OtpUriParserError.kt
│ │ │ │ └── OtpUriParserResult.kt
│ │ │ └── transformer
│ │ │ │ ├── DefaultKeyTransformer.kt
│ │ │ │ └── KeyTransformer.kt
│ │ └── settings
│ │ │ ├── DefaultSettings.kt
│ │ │ ├── Settings.kt
│ │ │ └── model
│ │ │ ├── ColorSetting.kt
│ │ │ ├── SortSetting.kt
│ │ │ └── ThemeSetting.kt
│ │ ├── db
│ │ ├── AccountDatabase.kt
│ │ ├── converter
│ │ │ ├── OtpConverter.kt
│ │ │ ├── UriConverter.kt
│ │ │ └── UuidConverter.kt
│ │ └── dao
│ │ │ ├── account
│ │ │ ├── AccountsDao.kt
│ │ │ └── entity
│ │ │ │ └── EntityAccount.kt
│ │ │ └── rtdata
│ │ │ ├── RtdataDao.kt
│ │ │ └── entity
│ │ │ └── EntityCountData.kt
│ │ ├── di
│ │ └── MauthDI.kt
│ │ ├── domain
│ │ ├── AuthRepository.kt
│ │ ├── QrRepository.kt
│ │ ├── SettingsRepository.kt
│ │ ├── account
│ │ │ ├── AccountRepository.kt
│ │ │ └── model
│ │ │ │ ├── DomainAccount.kt
│ │ │ │ ├── DomainAccountInfo.kt
│ │ │ │ └── DomainExportAccount.kt
│ │ └── otp
│ │ │ ├── OtpRepository.kt
│ │ │ └── model
│ │ │ └── DomainOtpRealtimeData.kt
│ │ ├── ui
│ │ ├── MainActivity.kt
│ │ ├── component
│ │ │ ├── Biometric.kt
│ │ │ ├── ResponsiveAppBarScaffold.kt
│ │ │ ├── Shapes.kt
│ │ │ ├── TwoPaneCard.kt
│ │ │ ├── Uri.kt
│ │ │ ├── UriImage.kt
│ │ │ ├── form
│ │ │ │ ├── ComboBoxFormField.kt
│ │ │ │ ├── Form.kt
│ │ │ │ ├── FormField.kt
│ │ │ │ ├── IntFormField.kt
│ │ │ │ ├── LazyForm.kt
│ │ │ │ ├── PasswordFormField.kt
│ │ │ │ └── TextFormField.kt
│ │ │ ├── lazygroup
│ │ │ │ ├── GroupedItemHeader.kt
│ │ │ │ ├── GroupedItemType.kt
│ │ │ │ └── GroupedListItem.kt
│ │ │ └── pinboard
│ │ │ │ ├── PinBoard.kt
│ │ │ │ ├── PinButton.kt
│ │ │ │ ├── PinDisplay.kt
│ │ │ │ └── PinScaffold.kt
│ │ ├── navigation
│ │ │ └── MauthDestination.kt
│ │ ├── screen
│ │ │ ├── about
│ │ │ │ ├── AboutLinks.kt
│ │ │ │ ├── AboutScreen.kt
│ │ │ │ └── component
│ │ │ │ │ └── LinkedButtons.kt
│ │ │ ├── account
│ │ │ │ ├── AccountForm.kt
│ │ │ │ ├── AccountScreen.kt
│ │ │ │ ├── AccountScreenState.kt
│ │ │ │ ├── AccountViewModel.kt
│ │ │ │ ├── IconFormField.kt
│ │ │ │ ├── component
│ │ │ │ │ ├── AccountComboBox.kt
│ │ │ │ │ ├── AccountDataField.kt
│ │ │ │ │ ├── AccountExitDialog.kt
│ │ │ │ │ └── AccountNumberField.kt
│ │ │ │ └── state
│ │ │ │ │ ├── AccountScreenError.kt
│ │ │ │ │ ├── AccountScreenLoading.kt
│ │ │ │ │ └── AccountScreenSuccess.kt
│ │ │ ├── auth
│ │ │ │ ├── AuthScreen.kt
│ │ │ │ └── AuthViewModel.kt
│ │ │ ├── export
│ │ │ │ ├── ExportScreen.kt
│ │ │ │ ├── ExportScreenState.kt
│ │ │ │ ├── ExportViewModel.kt
│ │ │ │ ├── component
│ │ │ │ │ └── ZxingQrImage.kt
│ │ │ │ └── state
│ │ │ │ │ ├── ExportScreenError.kt
│ │ │ │ │ ├── ExportScreenLoading.kt
│ │ │ │ │ └── ExportScreenSuccess.kt
│ │ │ ├── home
│ │ │ │ ├── HomeAddAccountMenu.kt
│ │ │ │ ├── HomeMoreMenu.kt
│ │ │ │ ├── HomeScreen.kt
│ │ │ │ ├── HomeScreenState.kt
│ │ │ │ ├── HomeViewModel.kt
│ │ │ │ ├── component
│ │ │ │ │ ├── HomeAccountCard.kt
│ │ │ │ │ ├── HomeAddAccountSheet.kt
│ │ │ │ │ ├── HomeDeleteAccountsDialog.kt
│ │ │ │ │ └── HomeScaffold.kt
│ │ │ │ └── state
│ │ │ │ │ ├── HomeScreenEmpty.kt
│ │ │ │ │ ├── HomeScreenError.kt
│ │ │ │ │ ├── HomeScreenLoading.kt
│ │ │ │ │ └── HomeScreenSuccess.kt
│ │ │ ├── pinremove
│ │ │ │ ├── PinRemoveScreen.kt
│ │ │ │ ├── PinRemoveScreenState.kt
│ │ │ │ └── PinRemoveViewModel.kt
│ │ │ ├── pinsetup
│ │ │ │ ├── PinSetupScreen.kt
│ │ │ │ ├── PinSetupScreenState.kt
│ │ │ │ └── PinSetupViewModel.kt
│ │ │ ├── qrscan
│ │ │ │ ├── QrScanScreen.kt
│ │ │ │ ├── QrScanViewModel.kt
│ │ │ │ ├── component
│ │ │ │ │ ├── QrScanCamera.kt
│ │ │ │ │ └── QrScanPermissionDeniedDialog.kt
│ │ │ │ └── state
│ │ │ │ │ ├── QrScanPermissionDenied.kt
│ │ │ │ │ └── QrScanPermissionGranted.kt
│ │ │ ├── settings
│ │ │ │ ├── SettingsScreen.kt
│ │ │ │ ├── SettingsViewModel.kt
│ │ │ │ └── component
│ │ │ │ │ ├── SettingsItem.kt
│ │ │ │ │ ├── SettingsNavigateItem.kt
│ │ │ │ │ └── SettingsSwitchItem.kt
│ │ │ └── theme
│ │ │ │ ├── ThemeScreen.kt
│ │ │ │ ├── ThemeViewModel.kt
│ │ │ │ └── component
│ │ │ │ └── ThemeColorCard.kt
│ │ ├── theme
│ │ │ ├── Theme.kt
│ │ │ ├── Type.kt
│ │ │ └── color
│ │ │ │ ├── BlueberryBlue.kt
│ │ │ │ ├── LemonYellow.kt
│ │ │ │ ├── LimeGreen.kt
│ │ │ │ ├── MothPurple.kt
│ │ │ │ ├── OrangeOrange.kt
│ │ │ │ └── SkyCyan.kt
│ │ └── util
│ │ │ └── FlowProducers.kt
│ │ └── util
│ │ └── Coroutines.kt
│ └── res
│ ├── drawable
│ ├── ic_add.xml
│ ├── ic_add_a_photo.xml
│ ├── ic_apartment.xml
│ ├── ic_arrow_back.xml
│ ├── ic_arrow_downward.xml
│ ├── ic_arrow_upward.xml
│ ├── ic_backspace.xml
│ ├── ic_brush.xml
│ ├── ic_bug.xml
│ ├── ic_check.xml
│ ├── ic_close.xml
│ ├── ic_contrast.xml
│ ├── ic_copy_all.xml
│ ├── ic_delete_forever.xml
│ ├── ic_edit.xml
│ ├── ic_empty_dashboard.xml
│ ├── ic_error.xml
│ ├── ic_export.xml
│ ├── ic_fingerprint.xml
│ ├── ic_github.xml
│ ├── ic_info.xml
│ ├── ic_key.xml
│ ├── ic_keyboard_arrow_down.xml
│ ├── ic_label.xml
│ ├── ic_launcher_foreground.xml
│ ├── ic_moon.xml
│ ├── ic_more_vert.xml
│ ├── ic_navigate_next.xml
│ ├── ic_password.xml
│ ├── ic_qr_code_2.xml
│ ├── ic_qr_code_scanner.xml
│ ├── ic_settings.xml
│ ├── ic_sort.xml
│ ├── ic_sun.xml
│ ├── ic_tab.xml
│ ├── ic_undo.xml
│ ├── ic_visibility.xml
│ └── ic_visibility_off.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ ├── ic_launcher_foreground.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ ├── ic_launcher_foreground.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ ├── ic_launcher_foreground.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ ├── ic_launcher_foreground.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ ├── ic_launcher_foreground.webp
│ └── ic_launcher_round.webp
│ ├── values-fr-rFR
│ └── strings.xml
│ ├── values-nb-rNO
│ └── strings.xml
│ ├── values-pt-rBR
│ └── strings.xml
│ ├── values-ru
│ └── strings.xml
│ ├── values-tr-rTR
│ └── strings.xml
│ ├── values-zh-rCN
│ └── strings.xml
│ ├── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── build.gradle.kts
├── fastlane
└── metadata
│ └── android
│ ├── en-US
│ ├── changelogs
│ │ ├── 10.txt
│ │ ├── 20.txt
│ │ ├── 21.txt
│ │ ├── 30.txt
│ │ ├── 40.txt
│ │ ├── 50.txt
│ │ ├── 51.txt
│ │ ├── 52.txt
│ │ ├── 53.txt
│ │ ├── 60.txt
│ │ ├── 61.txt
│ │ ├── 70.txt
│ │ ├── 80.txt
│ │ └── 90.txt
│ ├── full_description.txt
│ ├── images
│ │ ├── icon.png
│ │ └── phoneScreenshots
│ │ │ ├── 1.png
│ │ │ ├── 10.png
│ │ │ ├── 2.png
│ │ │ ├── 3.png
│ │ │ ├── 4.png
│ │ │ ├── 5.png
│ │ │ ├── 6.png
│ │ │ ├── 7.png
│ │ │ ├── 8.png
│ │ │ └── 9.png
│ ├── short_description.txt
│ └── title.txt
│ └── ru
│ ├── full_description.txt
│ └── short_description.txt
├── github
├── get_it_on_github.png
└── mauth.png
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build APK
2 |
3 | on:
4 | push:
5 | branches:
6 | - '*'
7 | paths-ignore:
8 | - '**.md'
9 | pull_request:
10 | branches:
11 | - '*'
12 | paths-ignore:
13 | - '**.md'
14 | workflow_dispatch:
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v2
21 |
22 | - name: Set up JDK 17
23 | uses: actions/setup-java@v2
24 | with:
25 | java-version: 17
26 | distribution: 'zulu'
27 | cache: 'gradle'
28 |
29 | - name: chmod gradlew
30 | run: chmod +x gradlew
31 |
32 | - name: Build the APK
33 | run: ./gradlew assembleDebug
34 |
35 | - name: Upload the APK
36 | uses: actions/upload-artifact@v4
37 | with:
38 | name: mauth
39 | path: app/build/outputs/apk/debug/app-debug.apk
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | .DS_Store
5 | build
6 | captures
7 | .externalNativeBuild
8 | .cxx
9 | local.properties
10 | app/release
--------------------------------------------------------------------------------
/POLICY:
--------------------------------------------------------------------------------
1 | Privacy Policy
2 | This privacy policy applies to the Mauth app (hereby referred to as "Application") for mobile devices that was created by Tornike Khintibidze (hereby referred to as "Service Provider") as an Open Source service. This service is intended for use "AS IS".
3 |
4 | What information does the Application obtain and how is it used?
5 | The Application does not obtain any information when you download and use it. Registration is not required to use the Application.
6 |
7 | Does the Application collect precise real time location information of the device?
8 | This Application does not collect precise information about the location of your mobile device.
9 |
10 | Do third parties see and/or have access to information obtained by the Application?
11 | Since the Application does not collect any information, no data is shared with third parties.
12 |
13 | What are my opt-out rights?
14 | You can stop all collection of information by the Application easily by uninstalling it. You may use the standard uninstall processes as may be available as part of your mobile device or via the mobile application marketplace or network.
15 |
16 | Children
17 | The Application is not used to knowingly solicit data from or market to children under the age of 13.
18 |
19 | The Service Provider does not knowingly collect personally identifiable information from children. The Service Provider encourages all children to never submit any personally identifiable information through the Application and/or Services. The Service Provider encourage parents and legal guardians to monitor their children's Internet usage and to help enforce this Policy by instructing their children never to provide personally identifiable information through the Application and/or Services without their permission. If you have reason to believe that a child has provided personally identifiable information to the Service Provider through the Application and/or Services, please contact the Service Provider (tornike.khintibidze2@gmail.com) so that they will be able to take the necessary actions. You must also be at least 16 years of age to consent to the processing of your personally identifiable information in your country (in some countries we may allow your parent or guardian to do so on your behalf).
20 |
21 | Security
22 | The Service Provider is concerned about safeguarding the confidentiality of your information. However, since the Application does not collect any information, there is no risk of your data being accessed by unauthorized individuals.
23 |
24 | Changes
25 | This Privacy Policy may be updated from time to time for any reason. The Service Provider will notify you of any changes to their Privacy Policy by updating this page with the new Privacy Policy. You are advised to consult this Privacy Policy regularly for any changes, as continued use is deemed approval of all changes.
26 |
27 | This privacy policy is effective as of 2025-01-04
28 |
29 | Your Consent
30 | By using the Application, you are consenting to the processing of your information as set forth in this Privacy Policy now and as amended by the Service Provider.
31 |
32 | Contact Us
33 | If you have any questions regarding privacy while using the Application, or have questions about the practices, please contact the Service Provider via email at tornike.khintibidze2@gmail.com.
34 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/compose_stability.conf:
--------------------------------------------------------------------------------
1 | kotlin.enums.EnumEntries
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -dontwarn com.google.errorprone.annotations.Immutable
24 | -dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue
25 | -dontwarn com.google.errorprone.annotations.CheckReturnValue
26 | -dontwarn com.google.errorprone.annotations.RestrictedApi
--------------------------------------------------------------------------------
/app/schemas/com.xinto.mauth.db.AccountDatabase/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 1,
5 | "identityHash": "a7b633760ea5ecaa908edaf929f167eb",
6 | "entities": [
7 | {
8 | "tableName": "accounts",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `secret` TEXT NOT NULL, `label` TEXT NOT NULL, `issuer` TEXT NOT NULL, PRIMARY KEY(`id`))",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "BLOB",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "secret",
19 | "columnName": "secret",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "label",
25 | "columnName": "label",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | },
29 | {
30 | "fieldPath": "issuer",
31 | "columnName": "issuer",
32 | "affinity": "TEXT",
33 | "notNull": true
34 | }
35 | ],
36 | "primaryKey": {
37 | "columnNames": [
38 | "id"
39 | ],
40 | "autoGenerate": false
41 | },
42 | "indices": [],
43 | "foreignKeys": []
44 | }
45 | ],
46 | "views": [],
47 | "setupQueries": [
48 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
49 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a7b633760ea5ecaa908edaf929f167eb')"
50 | ]
51 | }
52 | }
--------------------------------------------------------------------------------
/app/schemas/com.xinto.mauth.db.AccountDatabase/2.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 2,
5 | "identityHash": "b71f0ec08f1e3c91ebda8a8bda8ddd40",
6 | "entities": [
7 | {
8 | "tableName": "accounts",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `secret` TEXT NOT NULL, `label` TEXT NOT NULL, `issuer` TEXT NOT NULL, `algorithm` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 0, `digits` INTEGER NOT NULL DEFAULT 6, `counter` INTEGER NOT NULL DEFAULT 0, `period` INTEGER NOT NULL DEFAULT 30, PRIMARY KEY(`id`))",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "BLOB",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "secret",
19 | "columnName": "secret",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "label",
25 | "columnName": "label",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | },
29 | {
30 | "fieldPath": "issuer",
31 | "columnName": "issuer",
32 | "affinity": "TEXT",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "algorithm",
37 | "columnName": "algorithm",
38 | "affinity": "INTEGER",
39 | "notNull": true,
40 | "defaultValue": "0"
41 | },
42 | {
43 | "fieldPath": "type",
44 | "columnName": "type",
45 | "affinity": "INTEGER",
46 | "notNull": true,
47 | "defaultValue": "0"
48 | },
49 | {
50 | "fieldPath": "digits",
51 | "columnName": "digits",
52 | "affinity": "INTEGER",
53 | "notNull": true,
54 | "defaultValue": "6"
55 | },
56 | {
57 | "fieldPath": "counter",
58 | "columnName": "counter",
59 | "affinity": "INTEGER",
60 | "notNull": true,
61 | "defaultValue": "0"
62 | },
63 | {
64 | "fieldPath": "period",
65 | "columnName": "period",
66 | "affinity": "INTEGER",
67 | "notNull": true,
68 | "defaultValue": "30"
69 | }
70 | ],
71 | "primaryKey": {
72 | "columnNames": [
73 | "id"
74 | ],
75 | "autoGenerate": false
76 | },
77 | "indices": [],
78 | "foreignKeys": []
79 | }
80 | ],
81 | "views": [],
82 | "setupQueries": [
83 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
84 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b71f0ec08f1e3c91ebda8a8bda8ddd40')"
85 | ]
86 | }
87 | }
--------------------------------------------------------------------------------
/app/schemas/com.xinto.mauth.db.AccountDatabase/3.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 3,
5 | "identityHash": "9c1e38cb1cec8c1fd74fa9a90eb7b79c",
6 | "entities": [
7 | {
8 | "tableName": "accounts",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `icon` TEXT, `secret` TEXT NOT NULL, `label` TEXT NOT NULL, `issuer` TEXT NOT NULL, `algorithm` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 0, `digits` INTEGER NOT NULL DEFAULT 6, `counter` INTEGER NOT NULL DEFAULT 0, `period` INTEGER NOT NULL DEFAULT 30, PRIMARY KEY(`id`))",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "BLOB",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "icon",
19 | "columnName": "icon",
20 | "affinity": "TEXT",
21 | "notNull": false
22 | },
23 | {
24 | "fieldPath": "secret",
25 | "columnName": "secret",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | },
29 | {
30 | "fieldPath": "label",
31 | "columnName": "label",
32 | "affinity": "TEXT",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "issuer",
37 | "columnName": "issuer",
38 | "affinity": "TEXT",
39 | "notNull": true
40 | },
41 | {
42 | "fieldPath": "algorithm",
43 | "columnName": "algorithm",
44 | "affinity": "INTEGER",
45 | "notNull": true,
46 | "defaultValue": "0"
47 | },
48 | {
49 | "fieldPath": "type",
50 | "columnName": "type",
51 | "affinity": "INTEGER",
52 | "notNull": true,
53 | "defaultValue": "0"
54 | },
55 | {
56 | "fieldPath": "digits",
57 | "columnName": "digits",
58 | "affinity": "INTEGER",
59 | "notNull": true,
60 | "defaultValue": "6"
61 | },
62 | {
63 | "fieldPath": "counter",
64 | "columnName": "counter",
65 | "affinity": "INTEGER",
66 | "notNull": true,
67 | "defaultValue": "0"
68 | },
69 | {
70 | "fieldPath": "period",
71 | "columnName": "period",
72 | "affinity": "INTEGER",
73 | "notNull": true,
74 | "defaultValue": "30"
75 | }
76 | ],
77 | "primaryKey": {
78 | "columnNames": [
79 | "id"
80 | ],
81 | "autoGenerate": false
82 | },
83 | "indices": [],
84 | "foreignKeys": []
85 | }
86 | ],
87 | "views": [],
88 | "setupQueries": [
89 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
90 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9c1e38cb1cec8c1fd74fa9a90eb7b79c')"
91 | ]
92 | }
93 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/xinto/mauth/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.xinto.mauth", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/xinto/mauth/OtpStringTest.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import com.xinto.mauth.core.otp.model.OtpDigest
5 | import com.xinto.mauth.core.otp.parser.DefaultOtpUriParser
6 | import com.xinto.mauth.core.otp.parser.OtpUriParserResult
7 | import org.junit.Assert
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 |
11 | @RunWith(AndroidJUnit4::class)
12 | class OtpStringTest {
13 |
14 | private val parser = DefaultOtpUriParser()
15 | private val testString = "otpauth://totp/account?secret=secret&issuer=issuer&algorithm=sha1&digits=6&period=30"
16 |
17 | @Test
18 | fun testStringValidation() {
19 | val parseResult = parser.parseOtpUri(testString)
20 | Assert.assertTrue(parseResult.toString(), parseResult is OtpUriParserResult.Success)
21 |
22 | val castResult = parseResult as OtpUriParserResult.Success
23 | Assert.assertTrue(castResult.data.label == "account")
24 | Assert.assertTrue(castResult.data.secret == "secret")
25 | Assert.assertTrue(castResult.data.issuer == "issuer")
26 | Assert.assertTrue(castResult.data.algorithm == OtpDigest.SHA1)
27 | Assert.assertTrue(castResult.data.digits ==6)
28 | Assert.assertTrue(castResult.data.period ==30)
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/Mauth.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth
2 |
3 | import android.app.Application
4 | import com.xinto.mauth.di.MauthDI
5 | import org.koin.android.ext.koin.androidContext
6 | import org.koin.core.context.startKoin
7 |
8 | class Mauth : Application() {
9 |
10 | override fun onCreate() {
11 | super.onCreate()
12 |
13 | startKoin {
14 | androidContext(this@Mauth)
15 |
16 | modules(MauthDI.all)
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/auth/AuthManager.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.auth
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface AuthManager {
6 |
7 | fun getCode(): Flow
8 |
9 | fun setCode(code: String)
10 |
11 | fun removeCode()
12 |
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/auth/DefaultAuthManager.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.auth
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import androidx.core.content.edit
6 | import androidx.security.crypto.EncryptedSharedPreferences
7 | import androidx.security.crypto.MasterKey
8 | import kotlinx.coroutines.channels.awaitClose
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.callbackFlow
11 |
12 | class DefaultAuthManager(
13 | context: Context
14 | ) : AuthManager {
15 |
16 | private val prefs = EncryptedSharedPreferences.create(
17 | context,
18 | "auth",
19 | MasterKey(context = context),
20 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
21 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
22 | )
23 |
24 | override fun getCode(): Flow {
25 | return callbackFlow {
26 | val listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
27 | if (key == KEY_CODE) {
28 | trySend(sharedPreferences.getString(KEY_CODE, null))
29 | }
30 | }
31 | send(prefs.getString(KEY_CODE, null))
32 | prefs.registerOnSharedPreferenceChangeListener(listener)
33 | awaitClose {
34 | prefs.unregisterOnSharedPreferenceChangeListener(listener)
35 | }
36 | }
37 | }
38 |
39 | override fun setCode(code: String) {
40 | prefs.edit {
41 | putString(KEY_CODE, code)
42 | }
43 | }
44 |
45 | override fun removeCode() {
46 | prefs.edit {
47 | remove(KEY_CODE)
48 | }
49 | }
50 |
51 | private companion object {
52 | const val KEY_CODE = "code"
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/camera/QrCodeAnalyzer.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.camera
2 |
3 | import androidx.camera.core.ImageAnalysis
4 | import androidx.camera.core.ImageProxy
5 | import com.google.zxing.NotFoundException
6 | import java.nio.ByteBuffer
7 |
8 | class QrCodeAnalyzer(
9 | private inline val onSuccess: (com.google.zxing.Result) -> Unit,
10 | private inline val onFail: (NotFoundException) -> Unit
11 | ) : ImageAnalysis.Analyzer {
12 |
13 | override fun analyze(image: ImageProxy) {
14 | image.use { imageProxy ->
15 | val data = imageProxy.planes[0].buffer.toByteArray()
16 |
17 | ZxingDecoder.decodeYuvLuminanceSource(
18 | data = data,
19 | dataWidth = imageProxy.width,
20 | dataHeight = imageProxy.height,
21 | onSuccess = onSuccess,
22 | onError = onFail
23 | )
24 | }
25 | }
26 |
27 | private fun ByteBuffer.toByteArray(): ByteArray {
28 | rewind()
29 | val bytes = ByteArray(remaining())
30 | get(bytes)
31 | return bytes
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/camera/ZxingDecoder.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.camera
2 |
3 | import com.google.zxing.BarcodeFormat
4 | import com.google.zxing.BinaryBitmap
5 | import com.google.zxing.DecodeHintType
6 | import com.google.zxing.LuminanceSource
7 | import com.google.zxing.MultiFormatReader
8 | import com.google.zxing.NotFoundException
9 | import com.google.zxing.PlanarYUVLuminanceSource
10 | import com.google.zxing.RGBLuminanceSource
11 | import com.google.zxing.Result
12 | import com.google.zxing.common.HybridBinarizer
13 |
14 | object ZxingDecoder {
15 |
16 | val reader = MultiFormatReader().apply {
17 | setHints(
18 | mapOf(
19 | DecodeHintType.POSSIBLE_FORMATS to arrayListOf(BarcodeFormat.QR_CODE),
20 | )
21 | )
22 | }
23 |
24 | inline fun decodeYuvLuminanceSource(
25 | data: ByteArray,
26 | dataWidth: Int,
27 | dataHeight: Int,
28 | width: Int = dataWidth,
29 | height: Int = dataHeight,
30 | left: Int = 0,
31 | top: Int = 0,
32 | reverseHorizontal: Boolean = false,
33 | onSuccess: (Result) -> T,
34 | onError: (NotFoundException) -> T
35 | ): T {
36 | val source = PlanarYUVLuminanceSource(
37 | /* yuvData = */
38 | data,
39 | /* dataWidth = */
40 | dataWidth,
41 | /* dataHeight = */
42 | dataHeight,
43 | /* left = */
44 | left,
45 | /* top = */
46 | top,
47 | /* width = */
48 | width,
49 | /* height = */
50 | height,
51 | /* reverseHorizontal = */
52 | reverseHorizontal,
53 | )
54 |
55 | return decodeSource(source, onSuccess, onError)
56 | }
57 |
58 | inline fun decodeRgbLuminanceSource(
59 | pixels: IntArray,
60 | width: Int,
61 | height: Int,
62 | onSuccess: (Result) -> T,
63 | onError: (NotFoundException) -> T
64 | ): T {
65 | val source = RGBLuminanceSource(
66 | /* width = */
67 | width,
68 | /* height = */
69 | height,
70 | /* pixels = */
71 | pixels,
72 | )
73 |
74 | return decodeSource(source, onSuccess, onError)
75 | }
76 |
77 | inline fun decodeSource(
78 | source: LuminanceSource,
79 | onSuccess: (Result) -> T,
80 | onError: (NotFoundException) -> T
81 | ): T {
82 | val bitmap = BinaryBitmap(HybridBinarizer(source))
83 |
84 | return try {
85 | onSuccess(reader.decodeWithState(bitmap))
86 | } catch (e: NotFoundException) {
87 | onError(e)
88 | }
89 | }
90 |
91 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/camera/ZxingEncoder.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.camera
2 |
3 | import android.graphics.Bitmap
4 | import androidx.annotation.ColorInt
5 | import com.google.zxing.BarcodeFormat
6 | import com.google.zxing.EncodeHintType
7 | import com.google.zxing.MultiFormatWriter
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.withContext
10 | import kotlin.coroutines.resume
11 | import kotlin.coroutines.suspendCoroutine
12 |
13 | object ZxingEncoder {
14 |
15 | private val writer = MultiFormatWriter()
16 |
17 | suspend fun encodeToBitmap(
18 | data: String,
19 | size: Int,
20 | @ColorInt backgroundColor: Int,
21 | @ColorInt dataColor: Int
22 | ): Bitmap {
23 | return withContext(Dispatchers.IO){
24 | suspendCoroutine { continuation ->
25 | val bitMatrix = writer.encode(
26 | /* contents = */ data,
27 | /* format = */ BarcodeFormat.QR_CODE,
28 | /* width = */ size,
29 | /* height = */ size,
30 | /* hints = */ mapOf(EncodeHintType.MARGIN to 2)
31 | )
32 | val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
33 | for (x in 0 until size) {
34 | for (y in 0 until size) {
35 | val hasData = bitMatrix.get(x, y)
36 | setPixel(x, y, if (hasData) dataColor else backgroundColor)
37 | }
38 | }
39 | }
40 | continuation.resume(bitmap)
41 | }
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/exporter/DefaultOtpExporter.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.exporter
2 |
3 | import android.net.Uri
4 | import com.xinto.mauth.core.otp.model.OtpData
5 | import com.xinto.mauth.core.otp.model.OtpType
6 |
7 | class DefaultOtpExporter : OtpExporter {
8 |
9 | override fun exportOtp(data: OtpData): String {
10 | val uriBuilder = Uri.Builder()
11 | .scheme("otpauth")
12 | .appendPath(data.label)
13 | .appendQueryParameter("secret", data.secret)
14 | .appendQueryParameter("algorithm", data.algorithm.name)
15 | .appendQueryParameter("digits", data.digits.toString())
16 |
17 | if (data.issuer.isNotBlank()) {
18 | uriBuilder.appendQueryParameter("issuer", data.issuer)
19 | }
20 |
21 | return when (data.type) {
22 | OtpType.TOTP -> {
23 | uriBuilder
24 | .authority("totp")
25 | .appendQueryParameter("period", data.period.toString())
26 | }
27 | OtpType.HOTP -> {
28 | uriBuilder
29 | .authority("hotp")
30 | .appendQueryParameter("counter", data.period.toString())
31 | }
32 | }.toString().also(::println)
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/exporter/OtpExporter.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.exporter
2 |
3 | import com.xinto.mauth.core.otp.model.OtpData
4 |
5 | interface OtpExporter {
6 |
7 | fun exportOtp(data: OtpData): String
8 |
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/generator/DefaultOtpGenerator.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.generator
2 |
3 | import com.xinto.mauth.core.otp.model.OtpDigest
4 | import java.nio.ByteBuffer
5 | import javax.crypto.Mac
6 | import javax.crypto.spec.SecretKeySpec
7 | import kotlin.math.floor
8 | import kotlin.math.pow
9 |
10 | class DefaultOtpGenerator : OtpGenerator {
11 |
12 | override fun generateHotp(
13 | secret: ByteArray,
14 | counter: Long,
15 | digits: Int,
16 | digest: OtpDigest
17 | ): String {
18 | val hash = Mac.getInstance(digest.algorithmName).let { mac ->
19 | val byteCounter = ByteBuffer.allocate(8)
20 | .putLong(counter)
21 | .array()
22 |
23 | mac.init(SecretKeySpec(secret, "RAW"))
24 | mac.doFinal(byteCounter)
25 | }
26 |
27 | val offset = hash[hash.size - 1].toInt() and 0xF
28 |
29 | val code = ((hash[offset].toInt() and 0x7f) shl 24) or
30 | ((hash[offset + 1].toInt() and 0xff) shl 16) or
31 | ((hash[offset + 2].toInt() and 0xff) shl 8) or
32 | ((hash[offset + 3].toInt() and 0xff))
33 |
34 | val paddedCode = (code % 10.0.pow(digits.toDouble())).toInt()
35 |
36 | return StringBuilder(paddedCode.toString()).apply {
37 | while (length < digits) {
38 | insert(0, "0")
39 | }
40 | }.toString()
41 | }
42 |
43 | override fun generateTotp(
44 | secret: ByteArray,
45 | interval: Long,
46 | seconds: Long,
47 | digits: Int,
48 | digest: OtpDigest
49 | ): String {
50 | val counter = floor((seconds / interval).toDouble()).toLong()
51 | return generateHotp(secret, counter, digits, digest)
52 | }
53 |
54 | private val OtpDigest.algorithmName: String
55 | get() {
56 | return when (this) {
57 | OtpDigest.SHA1 -> "HmacSHA1"
58 | OtpDigest.SHA256 -> "HmacSHA256"
59 | OtpDigest.SHA512 -> "HmacSHA512"
60 | }
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/generator/OtpGenerator.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.generator
2 |
3 | import com.xinto.mauth.core.otp.model.OtpDigest
4 |
5 | interface OtpGenerator {
6 |
7 | fun generateHotp(
8 | secret: ByteArray,
9 | counter: Long,
10 | digits: Int = 6,
11 | digest: OtpDigest = OtpDigest.SHA1
12 | ): String
13 |
14 | fun generateTotp(
15 | secret: ByteArray,
16 | interval: Long,
17 | seconds: Long,
18 | digits: Int = 6,
19 | digest: OtpDigest = OtpDigest.SHA1
20 | ): String
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/model/OtpData.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.model
2 |
3 | data class OtpData(
4 | val label: String,
5 | val issuer: String,
6 | val secret: String,
7 | val algorithm: OtpDigest,
8 | val type: OtpType,
9 | val digits: Int,
10 | val counter: Int?,
11 | val period: Int?,
12 | )
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/model/OtpDigest.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.model
2 |
3 | enum class OtpDigest {
4 | SHA1,
5 | SHA256,
6 | SHA512
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/model/OtpType.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.model
2 |
3 | enum class OtpType {
4 | HOTP,
5 | TOTP,
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/parser/DefaultOtpUriParser.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.parser
2 |
3 | import android.net.Uri
4 | import com.xinto.mauth.core.otp.model.OtpData
5 | import com.xinto.mauth.core.otp.model.OtpDigest
6 | import com.xinto.mauth.core.otp.model.OtpType
7 |
8 |
9 | class DefaultOtpUriParser : OtpUriParser {
10 |
11 | override fun parseOtpUri(keyUri: String): OtpUriParserResult {
12 | val uri = Uri.parse(keyUri)
13 |
14 | val protocol = uri.scheme?.lowercase()
15 | if (protocol != "otpauth") {
16 | return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_PROTOCOL)
17 | }
18 |
19 | val type = when (uri.host?.lowercase()) {
20 | "hotp" -> OtpType.HOTP
21 | "totp" -> OtpType.TOTP
22 | else -> return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_TYPE)
23 | }
24 |
25 | val label = try {
26 | uri.pathSegments[0]
27 | } catch (e: IndexOutOfBoundsException) {
28 | return OtpUriParserResult.Failure(OtpUriParserError.ERROR_MISSING_LABEL)
29 | }
30 |
31 | val paramSecret = uri.getQueryParameter("secret")
32 | ?: return OtpUriParserResult.Failure(OtpUriParserError.ERROR_MISSING_SECRET)
33 |
34 | val paramIssuer = uri.getQueryParameter("issuer") ?: ""
35 |
36 | val paramAlgorithm = uri.getQueryParameter("algorithm") ?: "SHA1"
37 | val algorithm = getDigestFromUriAlgorithm(paramAlgorithm)
38 | ?: return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_ALGORITHM)
39 |
40 | val paramDigits = uri.getQueryParameter("digits") ?: "6"
41 | val digits = paramDigits.toIntOrNull()
42 | ?: return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_DIGITS)
43 |
44 | val paramPeriod = uri.getQueryParameter("period") ?: "30"
45 | val period = try {
46 | if (type == OtpType.HOTP) null else paramPeriod.toInt()
47 | } catch (e: NumberFormatException) {
48 | return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_PERIOD)
49 | }
50 |
51 | val paramCounter = uri.getQueryParameter("counter")
52 | if (type == OtpType.HOTP && paramCounter == null) {
53 | return OtpUriParserResult.Failure(OtpUriParserError.ERROR_MISSING_COUNTER)
54 | }
55 | val counter = try {
56 | paramCounter?.toInt()
57 | } catch (e: NumberFormatException) {
58 | return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_COUNTER)
59 | }
60 |
61 | val otpData = OtpData(
62 | label = label,
63 | issuer = paramIssuer,
64 | secret = paramSecret,
65 | algorithm = algorithm,
66 | type = type,
67 | digits = digits,
68 | period = period,
69 | counter = counter,
70 | )
71 |
72 | return OtpUriParserResult.Success(otpData)
73 | }
74 |
75 | private fun getDigestFromUriAlgorithm(algorithm: String): OtpDigest? {
76 | return when (algorithm.uppercase()) {
77 | "SHA1" -> OtpDigest.SHA1
78 | "SHA256" -> OtpDigest.SHA256
79 | "SHA512" -> OtpDigest.SHA512
80 | else -> null
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/parser/OtpUriParser.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.parser
2 |
3 | interface OtpUriParser {
4 | fun parseOtpUri(keyUri: String): OtpUriParserResult
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/parser/OtpUriParserError.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.parser
2 |
3 | enum class OtpUriParserError {
4 | ERROR_INVALID_PROTOCOL,
5 | ERROR_INVALID_TYPE,
6 | ERROR_INVALID_ALGORITHM,
7 | ERROR_INVALID_DIGITS,
8 | ERROR_INVALID_PERIOD,
9 | ERROR_INVALID_COUNTER,
10 | ERROR_MISSING_LABEL,
11 | ERROR_MISSING_SECRET,
12 | ERROR_MISSING_COUNTER,
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/parser/OtpUriParserResult.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.parser
2 |
3 | import com.xinto.mauth.core.otp.model.OtpData
4 |
5 | sealed interface OtpUriParserResult {
6 | data class Success(val data: OtpData) : OtpUriParserResult
7 | data class Failure(val error: OtpUriParserError) : OtpUriParserResult
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/transformer/DefaultKeyTransformer.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.transformer
2 |
3 | import org.apache.commons.codec.binary.Base32
4 |
5 | class DefaultKeyTransformer : KeyTransformer {
6 |
7 | private val base32 = Base32()
8 |
9 | override fun transformToBytes(key: String): ByteArray {
10 | val trimmed = key.trim()
11 | .replace("-", "")
12 | .replace(" ", "")
13 | return base32.decode(trimmed)
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/otp/transformer/KeyTransformer.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.otp.transformer
2 |
3 | interface KeyTransformer {
4 |
5 | fun transformToBytes(key: String): ByteArray
6 |
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/settings/DefaultSettings.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.settings
2 |
3 | import android.content.Context
4 | import androidx.datastore.preferences.core.booleanPreferencesKey
5 | import androidx.datastore.preferences.core.edit
6 | import androidx.datastore.preferences.core.stringPreferencesKey
7 | import androidx.datastore.preferences.preferencesDataStore
8 | import com.xinto.mauth.core.settings.model.ColorSetting
9 | import com.xinto.mauth.core.settings.model.SortSetting
10 | import com.xinto.mauth.core.settings.model.ThemeSetting
11 | import kotlinx.coroutines.flow.Flow
12 | import kotlinx.coroutines.flow.map
13 |
14 | class DefaultSettings(context: Context) : Settings {
15 |
16 | private val Context.preferences by preferencesDataStore("preferences")
17 | private val preferences = context.preferences
18 |
19 | override fun getSecureMode(): Flow {
20 | return preferences.data.map {
21 | it[KEY_SECURE_MODE] ?: false
22 | }
23 | }
24 |
25 | override fun getUseBiometrics(): Flow {
26 | return preferences.data.map {
27 | it[KEY_USE_BIOMETRICS] ?: false
28 | }
29 | }
30 |
31 | override fun getSortMode(): Flow {
32 | return preferences.data.map {
33 | it[KEY_SORT_MODE]?.let { name ->
34 | SortSetting.valueOf(name)
35 | } ?: SortSetting.DEFAULT
36 | }
37 | }
38 |
39 | override fun getTheme(): Flow {
40 | return preferences.data.map { preferences ->
41 | preferences[KEY_THEME]?.let { name ->
42 | ThemeSetting.entries.find { it.name == name }
43 | } ?: ThemeSetting.DEFAULT
44 | }
45 | }
46 |
47 | override fun getColor(): Flow {
48 | return preferences.data.map { preferences ->
49 | preferences[KEY_COLOR]?.let { name ->
50 | ColorSetting.entries.find { it.name == name }
51 | } ?: ColorSetting.DEFAULT
52 | }
53 | }
54 |
55 | override suspend fun setSecureMode(value: Boolean) {
56 | preferences.edit {
57 | it[KEY_SECURE_MODE] = value
58 | }
59 | }
60 |
61 | override suspend fun setUseBiometrics(value: Boolean) {
62 | preferences.edit {
63 | it[KEY_USE_BIOMETRICS] = value
64 | }
65 | }
66 |
67 | override suspend fun setSortMode(value: SortSetting) {
68 | preferences.edit {
69 | it[KEY_SORT_MODE] = value.name
70 | }
71 | }
72 |
73 | override suspend fun setTheme(value: ThemeSetting) {
74 | preferences.edit {
75 | it[KEY_THEME] = value.name
76 | }
77 | }
78 |
79 | override suspend fun setColor(value: ColorSetting) {
80 | preferences.edit {
81 | it[KEY_COLOR] = value.name
82 | }
83 | }
84 |
85 | private companion object {
86 | val KEY_SECURE_MODE = booleanPreferencesKey("private_mode")
87 | val KEY_USE_BIOMETRICS = booleanPreferencesKey("use_biometrics")
88 | val KEY_SORT_MODE = stringPreferencesKey("sort_mode")
89 | val KEY_THEME = stringPreferencesKey("theme")
90 | val KEY_COLOR = stringPreferencesKey("color")
91 | }
92 |
93 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/settings/Settings.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.settings
2 |
3 | import com.xinto.mauth.core.settings.model.ColorSetting
4 | import com.xinto.mauth.core.settings.model.SortSetting
5 | import com.xinto.mauth.core.settings.model.ThemeSetting
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | interface Settings {
9 | fun getSecureMode(): Flow
10 | fun getUseBiometrics(): Flow
11 | fun getSortMode(): Flow
12 | fun getTheme(): Flow
13 | fun getColor(): Flow
14 |
15 | suspend fun setSecureMode(value: Boolean)
16 | suspend fun setUseBiometrics(value: Boolean)
17 | suspend fun setSortMode(value: SortSetting)
18 | suspend fun setTheme(value: ThemeSetting)
19 | suspend fun setColor(value: ColorSetting)
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/settings/model/ColorSetting.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.settings.model
2 |
3 | import android.os.Build
4 |
5 | enum class ColorSetting {
6 | Dynamic,
7 | MothPurple,
8 | BlueberryBlue,
9 | PickleYellow,
10 | ToxicGreen,
11 | LeatherOrange,
12 | OceanTurquoise;
13 |
14 | companion object {
15 | val DEFAULT = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Dynamic else MothPurple
16 |
17 | val validEntries = entries.filter {
18 | it != Dynamic || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/settings/model/SortSetting.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.settings.model
2 |
3 | enum class SortSetting {
4 | DateAsc,
5 | DateDesc,
6 | LabelAsc,
7 | LabelDesc,
8 | IssuerAsc,
9 | IssuerDesc;
10 |
11 | companion object {
12 | val DEFAULT = DateDesc
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/core/settings/model/ThemeSetting.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.core.settings.model
2 |
3 | enum class ThemeSetting {
4 | System,
5 | Dark,
6 | Light;
7 |
8 | companion object {
9 | val DEFAULT = System
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/db/AccountDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.db
2 |
3 | import androidx.room.AutoMigration
4 | import androidx.room.Database
5 | import androidx.room.RoomDatabase
6 | import androidx.room.TypeConverters
7 | import androidx.room.migration.Migration
8 | import androidx.sqlite.db.SupportSQLiteDatabase
9 | import com.xinto.mauth.db.converter.OtpConverter
10 | import com.xinto.mauth.db.converter.UriConverter
11 | import com.xinto.mauth.db.converter.UuidConverter
12 | import com.xinto.mauth.db.dao.account.AccountsDao
13 | import com.xinto.mauth.db.dao.account.entity.EntityAccount
14 | import com.xinto.mauth.db.dao.rtdata.RtdataDao
15 | import com.xinto.mauth.db.dao.rtdata.entity.EntityCountData
16 |
17 | @Database(
18 | entities = [EntityAccount::class, EntityCountData::class],
19 | version = 5,
20 | autoMigrations = [
21 | AutoMigration(
22 | from = 1,
23 | to = 2
24 | ),
25 | AutoMigration(
26 | from = 2,
27 | to = 3
28 | ),
29 | AutoMigration(
30 | from = 4,
31 | to = 5
32 | )
33 | ]
34 | )
35 | @TypeConverters(UuidConverter::class, OtpConverter::class, UriConverter::class)
36 | abstract class AccountDatabase : RoomDatabase() {
37 |
38 | abstract fun accountsDao(): AccountsDao
39 |
40 | abstract fun rtdataDao(): RtdataDao
41 |
42 | companion object Migrations {
43 |
44 | val Migrate3to4 = object : Migration(3, 4) {
45 | override fun migrate(database: SupportSQLiteDatabase) {
46 | database.execSQL("CREATE TABLE countdata (account_id BLOB NOT NULL, count INTEGER NOT NULL, PRIMARY KEY(account_id))")
47 | database.execSQL("INSERT INTO countdata (account_id, count) SELECT id, counter FROM accounts")
48 | database.execSQL("CREATE TABLE accounts_temp (id BLOB NOT NULL, icon TEXT, secret TEXT NOT NULL, label TEXT NOT NULL, issuer TEXT NOT NULL, algorithm INTEGER NOT NULL DEFAULT 0, type INTEGER NOT NULL DEFAULT 0, digits INTEGER NOT NULL DEFAULT 6, period INTEGER NOT NULL DEFAULT 30, PRIMARY KEY(id))")
49 | database.execSQL("INSERT INTO accounts_temp (id, icon, secret, label, issuer, algorithm, type, digits, period) SELECT id, icon, secret, label, issuer, algorithm, type, digits, period FROM accounts")
50 | database.execSQL("DROP TABLE accounts")
51 | database.execSQL("ALTER TABLE accounts_temp RENAME TO accounts")
52 | }
53 | }
54 |
55 | val Migrate4To5 = object : Migration(4, 5) {
56 | override fun migrate(database: SupportSQLiteDatabase) {
57 | database.execSQL("ALTER TABLE accounts ADD COLUMN create_date INTEGER NOT NULL DEFAULT 0")
58 | database.execSQL("UPDATE accounts SET create_date = strftime('%s','now') + ROWID")
59 | }
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/db/converter/OtpConverter.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.db.converter
2 |
3 | import androidx.room.TypeConverter
4 | import com.xinto.mauth.core.otp.model.OtpDigest
5 | import com.xinto.mauth.core.otp.model.OtpType
6 |
7 | class OtpConverter {
8 |
9 | @TypeConverter
10 | fun fromIntToDigest(value: Int): OtpDigest {
11 | return OtpDigest.values()[value]
12 | }
13 |
14 | @TypeConverter
15 | fun fromDigestToInt(digest: OtpDigest): Int {
16 | return digest.ordinal
17 | }
18 |
19 | @TypeConverter
20 | fun fromIntToType(value: Int): OtpType {
21 | return OtpType.values()[value]
22 | }
23 |
24 | @TypeConverter
25 | fun fromTypeToInt(digest: OtpType): Int {
26 | return digest.ordinal
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/db/converter/UriConverter.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.db.converter
2 |
3 | import android.net.Uri
4 | import androidx.room.TypeConverter
5 |
6 | class UriConverter {
7 |
8 | @TypeConverter
9 | fun convertUriToString(uri: Uri): String {
10 | return uri.toString()
11 | }
12 |
13 | @TypeConverter
14 | fun convertStringToUri(uriString: String): Uri {
15 | return Uri.parse(uriString)
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/db/converter/UuidConverter.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.db.converter
2 |
3 | import androidx.room.TypeConverter
4 | import java.nio.ByteBuffer
5 | import java.nio.ByteOrder
6 | import java.util.UUID
7 |
8 | class UuidConverter {
9 |
10 | @TypeConverter
11 | fun uuidToBytes(uuid: UUID): ByteArray {
12 | val bytes = ByteArray(16)
13 |
14 | ByteBuffer.wrap(bytes)
15 | .order(ByteOrder.BIG_ENDIAN)
16 | .putLong(uuid.mostSignificantBits)
17 | .putLong(uuid.leastSignificantBits)
18 |
19 | return bytes
20 | }
21 |
22 | @TypeConverter
23 | fun bytesToUuid(uuid: ByteArray): UUID {
24 | val buffer = ByteBuffer.wrap(uuid)
25 |
26 | return UUID(buffer.long, buffer.long)
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/db/dao/account/AccountsDao.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.db.dao.account
2 |
3 | import androidx.room.*
4 | import com.xinto.mauth.db.dao.account.entity.EntityAccount
5 | import kotlinx.coroutines.flow.Flow
6 | import java.util.*
7 |
8 | @Dao
9 | interface AccountsDao {
10 |
11 | @Query("SELECT * FROM accounts")
12 | fun observeAll(): Flow>
13 |
14 | @Query("SELECT * FROM accounts")
15 | suspend fun getAll(): List
16 |
17 | @Query("SELECT * FROM accounts WHERE id = :id")
18 | suspend fun getById(id: UUID): EntityAccount?
19 |
20 | @Upsert
21 | suspend fun upsert(entityAccount: EntityAccount)
22 |
23 | @Delete
24 | suspend fun delete(entityAccount: EntityAccount)
25 |
26 | @Query("DELETE FROM accounts WHERE id in (:ids)")
27 | suspend fun delete(ids: Set)
28 |
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/db/dao/account/entity/EntityAccount.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.db.dao.account.entity
2 |
3 | import android.net.Uri
4 | import androidx.room.ColumnInfo
5 | import androidx.room.Entity
6 | import androidx.room.PrimaryKey
7 | import com.xinto.mauth.core.otp.model.OtpDigest
8 | import com.xinto.mauth.core.otp.model.OtpType
9 | import java.util.UUID
10 |
11 | @Entity(tableName = "accounts")
12 | data class EntityAccount(
13 | @PrimaryKey
14 | @ColumnInfo(name = "id", typeAffinity = ColumnInfo.BLOB)
15 | val id: UUID = UUID.randomUUID(),
16 |
17 | @ColumnInfo(name = "icon")
18 | val icon: Uri?,
19 |
20 | @ColumnInfo(name = "secret")
21 | val secret: String,
22 |
23 | @ColumnInfo(name = "label")
24 | val label: String,
25 |
26 | @ColumnInfo(name = "issuer")
27 | val issuer: String,
28 |
29 | @ColumnInfo(name = "algorithm", defaultValue = "0")
30 | val algorithm: OtpDigest,
31 |
32 | @ColumnInfo(name = "type", defaultValue = "0")
33 | val type: OtpType,
34 |
35 | @ColumnInfo(name = "digits", defaultValue = "6")
36 | val digits: Int,
37 |
38 | @ColumnInfo(name = "period", defaultValue = "30")
39 | val period: Int,
40 |
41 | @ColumnInfo(name = "create_date", defaultValue = "0")
42 | val createDateMillis: Long
43 | )
44 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/db/dao/rtdata/RtdataDao.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.db.dao.rtdata
2 |
3 | import androidx.room.*
4 | import com.xinto.mauth.db.dao.rtdata.entity.EntityCountData
5 | import kotlinx.coroutines.flow.Flow
6 | import java.util.UUID
7 |
8 | @Dao
9 | interface RtdataDao {
10 |
11 | @Query("SELECT * FROM countdata")
12 | fun observeCountData(): Flow>
13 |
14 | @Query("SELECT * FROM countdata WHERE account_id = :accountId")
15 | fun observeAccountCountData(accountId: UUID): Flow
16 |
17 | @Query("SELECT count FROM countdata WHERE account_id = :accountId")
18 | suspend fun getAccountCounter(accountId: UUID): Int
19 |
20 | @Upsert
21 | suspend fun upsertCountData(countData: EntityCountData)
22 |
23 | @Query("UPDATE countdata SET count = count + 1 WHERE account_id = :accountId")
24 | suspend fun incrementAccountCounter(accountId: UUID)
25 |
26 | @Query("UPDATE countdata SET count = :counter WHERE account_id = :accountId")
27 | suspend fun setAccountCounter(accountId: UUID, counter: Int)
28 |
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/db/dao/rtdata/entity/EntityCountData.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.db.dao.rtdata.entity
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 | import java.util.UUID
7 |
8 | @Entity(tableName = "countdata")
9 | data class EntityCountData(
10 | @PrimaryKey
11 | @ColumnInfo(name = "account_id")
12 | val accountId: UUID,
13 |
14 | @ColumnInfo(name = "count")
15 | val count: Int,
16 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/domain/AuthRepository.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.domain
2 |
3 | import com.xinto.mauth.core.auth.AuthManager
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.first
6 | import kotlinx.coroutines.flow.map
7 |
8 | class AuthRepository(
9 | private val authManager: AuthManager
10 | ) {
11 |
12 | private val liveCode = authManager.getCode()
13 |
14 | fun observeIsProtected(): Flow {
15 | return liveCode.map { it != null }
16 | }
17 |
18 | suspend fun isProtected(): Boolean {
19 | return liveCode.first() != null
20 | }
21 |
22 | suspend fun validate(code: String): Boolean {
23 | return liveCode.first() == code
24 | }
25 |
26 | fun updateCode(code: String) {
27 | authManager.setCode(code)
28 | }
29 |
30 | fun removeCode() {
31 | authManager.removeCode()
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/domain/QrRepository.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.domain
2 |
3 | import android.graphics.Bitmap
4 | import com.xinto.mauth.core.camera.ZxingDecoder
5 |
6 | class QrRepository {
7 |
8 | fun decodeQrImage(image: Bitmap): String? {
9 | val pixels = IntArray(image.width * image.height)
10 | image.getPixels(pixels, 0, image.width, 0, 0, image.width, image.height)
11 | return ZxingDecoder.decodeRgbLuminanceSource(
12 | pixels = pixels,
13 | width = image.width,
14 | height = image.height,
15 | onSuccess = {
16 | return it.text
17 | },
18 | onError = {
19 | null
20 | }
21 | )
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/domain/SettingsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.domain
2 |
3 | import com.xinto.mauth.core.settings.Settings
4 |
5 | class SettingsRepository(private val settings: Settings) : Settings by settings
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/domain/account/model/DomainAccount.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.domain.account.model
2 |
3 | import android.net.Uri
4 | import androidx.compose.runtime.Immutable
5 | import com.xinto.mauth.core.otp.model.OtpDigest
6 | import java.util.UUID
7 |
8 | @Immutable
9 | sealed class DomainAccount {
10 | abstract val id: UUID
11 | abstract val icon: Uri?
12 | abstract val secret: String
13 | abstract val label: String
14 | abstract val issuer: String
15 | abstract val algorithm: OtpDigest
16 | abstract val digits: Int
17 | abstract val createdMillis: Long
18 |
19 | val shortLabel by lazy {
20 | label.filter {
21 | it.isUpperCase()
22 | }.ifEmpty {
23 | label[0].uppercase()
24 | }.take(3)
25 | }
26 |
27 | @Immutable
28 | data class Totp(
29 | override val id: UUID,
30 | override val icon: Uri?,
31 | override val secret: String,
32 | override val label: String,
33 | override val issuer: String,
34 | override val algorithm: OtpDigest,
35 | override val digits: Int,
36 | override val createdMillis: Long,
37 | val period: Int
38 | ) : DomainAccount()
39 |
40 | @Immutable
41 | data class Hotp(
42 | override val id: UUID,
43 | override val icon: Uri?,
44 | override val secret: String,
45 | override val label: String,
46 | override val issuer: String,
47 | override val algorithm: OtpDigest,
48 | override val digits: Int,
49 | override val createdMillis: Long,
50 | ) : DomainAccount()
51 |
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/domain/account/model/DomainAccountInfo.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.domain.account.model
2 |
3 | import android.net.Uri
4 | import android.os.Parcelable
5 | import androidx.compose.runtime.Immutable
6 | import com.xinto.mauth.core.otp.model.OtpDigest
7 | import com.xinto.mauth.core.otp.model.OtpType
8 | import kotlinx.parcelize.Parcelize
9 | import java.util.UUID
10 |
11 | @Immutable
12 | @Parcelize
13 | data class DomainAccountInfo(
14 | val id: UUID,
15 | val icon: Uri?,
16 | val label: String,
17 | val issuer: String,
18 | val secret: String,
19 | val algorithm: OtpDigest,
20 | val type: OtpType,
21 | val digits: Int,
22 | val counter: Int,
23 | val period: Int,
24 | val createdMillis: Long
25 | ) : Parcelable {
26 |
27 | companion object {
28 | fun new(): DomainAccountInfo {
29 | return DomainAccountInfo(
30 | id = UUID.randomUUID(),
31 | icon = null,
32 | label = "",
33 | issuer = "",
34 | secret = "",
35 | algorithm = OtpDigest.SHA1,
36 | type = OtpType.TOTP,
37 | digits = 6,
38 | counter = 0,
39 | period = 30,
40 | createdMillis = System.currentTimeMillis()
41 | )
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/domain/account/model/DomainExportAccount.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.domain.account.model
2 |
3 | import android.net.Uri
4 | import java.util.UUID
5 |
6 | data class DomainExportAccount(
7 | val id: UUID,
8 | val icon: Uri?,
9 | val label: String,
10 | val issuer: String,
11 | val url: String
12 | ) {
13 | val shortLabel = label.take(1)
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/domain/otp/model/DomainOtpRealtimeData.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.domain.otp.model
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | @Immutable
6 | sealed interface DomainOtpRealtimeData {
7 | val code: String
8 |
9 | @Immutable
10 | data class Totp(
11 | override val code: String,
12 | val progress: Float,
13 | val countdown: Int,
14 | ) : DomainOtpRealtimeData
15 |
16 | @Immutable
17 | data class Hotp(
18 | override val code: String,
19 | val count: Int,
20 | ) : DomainOtpRealtimeData
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/TwoPaneCard.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.combinedClickable
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.ElevatedCard
8 | import androidx.compose.material3.HorizontalDivider
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.unit.dp
14 |
15 | @Composable
16 | fun TwoPaneCard(
17 | selected: Boolean,
18 | modifier: Modifier = Modifier,
19 | expanded: Boolean = true,
20 | topContent: @Composable () -> Unit,
21 | bottomContent: @Composable () -> Unit,
22 | onClick: () -> Unit,
23 | onLongClick: () -> Unit,
24 | ) {
25 | val shape by animateRoundedCornerShapeAsState(
26 | targetValue = if (selected) MaterialTheme.shapes.small else MaterialTheme.shapes.large,
27 | )
28 | ElevatedCard(
29 | modifier = modifier,
30 | shape = shape,
31 | ) {
32 | Column(
33 | modifier = Modifier
34 | .combinedClickable(
35 | onClick = onClick,
36 | onLongClick = onLongClick,
37 | )
38 | .padding(12.dp)
39 | ) {
40 | topContent()
41 | AnimatedVisibility(
42 | visible = expanded,
43 | ) {
44 | Column {
45 | HorizontalDivider(Modifier.padding(vertical = 12.dp))
46 | bottomContent()
47 | }
48 | }
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/Uri.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.Immutable
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.ui.platform.LocalContext
10 |
11 | @Composable
12 | fun rememberUriHandler(): UriHandler {
13 | val context = LocalContext.current
14 | return remember(context) {
15 | UriHandler(context)
16 | }
17 | }
18 |
19 | @Immutable
20 | class UriHandler(private val context: Context) {
21 |
22 | fun openUrl(url: String) {
23 | context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/UriImage.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component
2 |
3 | import android.net.Uri
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.layout.ContentScale
8 | import androidx.compose.ui.platform.LocalContext
9 | import coil.compose.rememberAsyncImagePainter
10 | import coil.request.ImageRequest
11 |
12 | @Composable
13 | fun UriImage(
14 | uri: Uri,
15 | modifier: Modifier = Modifier,
16 | contentDescription: String? = null
17 | ) {
18 | val context = LocalContext.current
19 | Image(
20 | modifier = modifier,
21 | painter = rememberAsyncImagePainter(
22 | model = ImageRequest.Builder(context)
23 | .data(uri)
24 | .build()
25 | ),
26 | contentDescription = contentDescription,
27 | contentScale = ContentScale.Crop
28 | )
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/form/ComboBoxFormField.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component.form
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.animation.core.animateFloatAsState
5 | import androidx.compose.material3.DropdownMenuItem
6 | import androidx.compose.material3.ExposedDropdownMenuBox
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.MenuAnchorType
9 | import androidx.compose.material3.OutlinedTextField
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.runtime.mutableStateOf
14 | import androidx.compose.runtime.remember
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.rotate
17 | import androidx.compose.ui.res.painterResource
18 | import androidx.compose.ui.res.stringResource
19 | import com.xinto.mauth.R
20 |
21 | class ComboBoxFormField>(
22 | initial: E,
23 |
24 | @StringRes
25 | private val label: Int
26 | ) : FormField(initial, id = label) {
27 |
28 | private val clazz = initial.declaringJavaClass
29 |
30 | @Composable
31 | override fun invoke(modifier: Modifier) {
32 | val (expanded, setExpanded) = remember {
33 | mutableStateOf(false)
34 | }
35 | ExposedDropdownMenuBox(
36 | expanded = expanded,
37 | onExpandedChange = setExpanded
38 | ) {
39 | OutlinedTextField(
40 | modifier = modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
41 | value = value.name,
42 | onValueChange = {},
43 | singleLine = true,
44 | label = {
45 | Text(stringResource(label))
46 | },
47 | readOnly = true,
48 | trailingIcon = {
49 | val iconRotation by animateFloatAsState(if (expanded) 180f else 0f)
50 | Icon(
51 | modifier = Modifier.rotate(iconRotation),
52 | painter = painterResource(R.drawable.ic_keyboard_arrow_down),
53 | contentDescription = null
54 | )
55 | },
56 | isError = error
57 | )
58 | ExposedDropdownMenu(
59 | expanded = expanded,
60 | onDismissRequest = { setExpanded(false) }
61 | ) {
62 | clazz.enumConstants!!.forEach {
63 | DropdownMenuItem(
64 | text = { Text(it.name) },
65 | onClick = {
66 | setExpanded(false)
67 | value = it
68 | }
69 | )
70 | }
71 | }
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/form/Form.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component.form
2 |
3 | abstract class Form {
4 |
5 | abstract fun validate(): T?
6 |
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/form/FormField.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component.form
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.compose.ui.Modifier
9 |
10 | @Stable
11 | abstract class FormField(
12 | initial: T,
13 | val id: Int
14 | ) {
15 |
16 | var value by mutableStateOf(initial)
17 | protected set
18 |
19 | var error by mutableStateOf(false)
20 | private set
21 |
22 | @Composable
23 | abstract operator fun invoke(modifier: Modifier = Modifier)
24 |
25 | fun validate(): Boolean {
26 | return isValid().also {
27 | error = !it
28 | }
29 | }
30 |
31 | protected open fun isValid(): Boolean {
32 | return true
33 | }
34 |
35 | override fun equals(other: Any?): Boolean {
36 | if (this === other) return true
37 | if (other !is FormField<*>) return false
38 |
39 | if (id != other.id) return false
40 |
41 | return true
42 | }
43 |
44 | override fun hashCode(): Int {
45 | return id
46 | }
47 |
48 |
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/form/IntFormField.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component.form
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.foundation.text.KeyboardOptions
5 | import androidx.compose.material3.OutlinedTextField
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.res.stringResource
10 | import androidx.compose.ui.text.input.KeyboardType
11 | import com.xinto.mauth.R
12 |
13 | class IntFormField(
14 | initial: Int,
15 |
16 | @StringRes
17 | private val label: Int,
18 |
19 | private val min: Int = Int.MIN_VALUE,
20 | private val max: Int = Int.MAX_VALUE
21 | ) : FormField(initial.toString(), id = label) {
22 |
23 | @Composable
24 | override fun invoke(modifier: Modifier) {
25 | OutlinedTextField(
26 | modifier = modifier,
27 | value = value,
28 | onValueChange = {
29 | value = it
30 | },
31 | label = {
32 | Text(stringResource(label))
33 | },
34 | supportingText = if (max == Int.MAX_VALUE) null else { ->
35 | Text(stringResource(R.string.account_data_status_range, min.toString(), max.toString()))
36 | },
37 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
38 | isError = error
39 | )
40 | }
41 |
42 | override fun isValid(): Boolean {
43 | val intValue = value.toIntOrNull() ?: return false
44 |
45 | return intValue in min..max
46 | }
47 |
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/form/LazyForm.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component.form
2 |
3 | import androidx.compose.foundation.lazy.grid.GridItemSpan
4 | import androidx.compose.foundation.lazy.grid.LazyGridScope
5 |
6 | interface LazyGridForm {
7 |
8 | operator fun LazyGridScope.invoke()
9 |
10 | }
11 |
12 | fun LazyGridScope.form(lazyGridForm: LazyGridForm) {
13 | with(lazyGridForm) {
14 | this@form.invoke()
15 | }
16 | }
17 |
18 | inline fun > LazyGridScope.formfield(field: T) {
19 | item(
20 | contentType = { field },
21 | key = field.id
22 | ) {
23 | field()
24 | }
25 | }
26 | inline fun > LazyGridScope.singleFormfield(field: T) {
27 | item(
28 | contentType = field,
29 | key = field.id,
30 | span = { GridItemSpan(maxCurrentLineSpan) }
31 | ) {
32 | field()
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/form/PasswordFormField.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component.form
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.StringRes
5 | import androidx.compose.material3.Icon
6 | import androidx.compose.material3.IconButton
7 | import androidx.compose.material3.OutlinedTextField
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.saveable.rememberSaveable
14 | import androidx.compose.runtime.setValue
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.res.painterResource
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.compose.ui.text.input.PasswordVisualTransformation
19 | import androidx.compose.ui.text.input.VisualTransformation
20 | import com.xinto.mauth.R
21 |
22 | class PasswordFormField(
23 | initial: String,
24 |
25 | @StringRes
26 | private val label: Int,
27 |
28 | @DrawableRes
29 | private val icon: Int,
30 | private val required: Boolean = false
31 | ) : FormField(initial, id = label) {
32 |
33 | @Composable
34 | override fun invoke(modifier: Modifier) {
35 | var showPassword by rememberSaveable { mutableStateOf(false) }
36 | val visualTransformation = remember(showPassword) {
37 | if (showPassword) {
38 | VisualTransformation.None
39 | } else {
40 | PasswordVisualTransformation()
41 | }
42 | }
43 | OutlinedTextField(
44 | modifier = modifier,
45 | value = value,
46 | onValueChange = {
47 | value = it
48 | },
49 | label = {
50 | Text(stringResource(label))
51 | },
52 | leadingIcon = if (icon == 0) null else { ->
53 | Icon(
54 | painter = painterResource(icon),
55 | contentDescription = null
56 | )
57 | },
58 | trailingIcon = {
59 | IconButton(onClick = { showPassword = !showPassword }) {
60 | val visible = painterResource(R.drawable.ic_visibility)
61 | val notVisible = painterResource(R.drawable.ic_visibility_off)
62 | Icon(
63 | painter = if (showPassword) visible else notVisible,
64 | contentDescription = null
65 | )
66 | }
67 | },
68 | supportingText = if (!required) null else { ->
69 | Text(stringResource(R.string.account_data_status_required))
70 | },
71 | visualTransformation = visualTransformation,
72 | isError = error
73 | )
74 | }
75 |
76 | override fun isValid(): Boolean {
77 | if (!required) return true
78 |
79 | return value.isNotEmpty()
80 | }
81 |
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/form/TextFormField.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component.form
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.StringRes
5 | import androidx.compose.material3.Icon
6 | import androidx.compose.material3.OutlinedTextField
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.painterResource
11 | import androidx.compose.ui.res.stringResource
12 | import com.xinto.mauth.R
13 |
14 | class TextFormField(
15 | initial: String,
16 |
17 | @StringRes
18 | private val label: Int,
19 |
20 | @DrawableRes
21 | private val icon: Int = 0,
22 | private val required: Boolean = false
23 | ) : FormField(initial, id = label) {
24 |
25 | @Composable
26 | override fun invoke(modifier: Modifier) {
27 | OutlinedTextField(
28 | modifier = modifier,
29 | value = value,
30 | onValueChange = {
31 | value = it
32 | },
33 | label = {
34 | Text(stringResource(label))
35 | },
36 | leadingIcon = if (icon == 0) null else { ->
37 | Icon(
38 | painter = painterResource(icon),
39 | contentDescription = null
40 | )
41 | },
42 | supportingText = if (!required) null else { ->
43 | Text(stringResource(R.string.account_data_status_required))
44 | },
45 | isError = error
46 | )
47 | }
48 |
49 | override fun isValid(): Boolean {
50 | if (!required) return true
51 |
52 | return value.isNotEmpty()
53 | }
54 |
55 |
56 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/lazygroup/GroupedItemType.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component.lazygroup
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 |
5 | enum class GroupedItemType {
6 | First,
7 | Middle,
8 | Last,
9 | Only
10 | }
11 |
12 | val LocalGroupedItemType = compositionLocalOf { GroupedItemType.Only }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/component/pinboard/PinDisplay.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.component.pinboard
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.animateContentSize
6 | import androidx.compose.animation.core.MutableTransitionState
7 | import androidx.compose.animation.scaleIn
8 | import androidx.compose.animation.scaleOut
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.Row
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.width
14 | import androidx.compose.foundation.shape.CircleShape
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Surface
17 | import androidx.compose.material3.Text
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.remember
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.platform.LocalInspectionMode
23 | import androidx.compose.ui.text.style.TextAlign
24 | import androidx.compose.ui.tooling.preview.Preview
25 | import androidx.compose.ui.unit.dp
26 | import com.xinto.mauth.ui.theme.MauthTheme
27 |
28 | @Composable
29 | fun PinDisplay(
30 | length: Int,
31 | modifier: Modifier = Modifier,
32 | error: Boolean = false,
33 | ) {
34 | val inspectionMode = LocalInspectionMode.current
35 | val color = when (error) {
36 | true -> MaterialTheme.colorScheme.errorContainer
37 | false -> MaterialTheme.colorScheme.secondaryContainer
38 | }
39 | Surface(
40 | modifier = modifier,
41 | color = color,
42 | shape = CircleShape
43 | ) {
44 | Row(
45 | modifier = Modifier
46 | .height(64.dp)
47 | .padding(horizontal = 8.dp)
48 | .animateContentSize(),
49 | horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
50 | verticalAlignment = Alignment.CenterVertically
51 | ) {
52 | for (i in 0.. = emptyList()
36 | ) : MauthDestination()
37 |
38 | @Parcelize
39 | data object PinSetup : MauthDestination()
40 |
41 | @Parcelize
42 | data object PinRemove : MauthDestination()
43 |
44 | @Parcelize
45 | data object Theme : MauthDestination()
46 |
47 | @Parcelize
48 | data object About : MauthDestination()
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/about/AboutLinks.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.about
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.StringRes
5 | import androidx.compose.runtime.Immutable
6 | import com.xinto.mauth.R
7 |
8 | @Immutable
9 | data class AboutLink(
10 | @DrawableRes
11 | val icon: Int,
12 |
13 | @StringRes
14 | val title: Int,
15 |
16 | val url: String
17 | ) {
18 | companion object {
19 | val defaultLinks = setOf(
20 | AboutLink(
21 | icon = R.drawable.ic_github,
22 | title = R.string.about_links_source,
23 | url = "https://github.com/X1nto/Mauth"
24 | ),
25 | AboutLink(
26 | icon = R.drawable.ic_bug,
27 | title = R.string.about_links_feedback,
28 | url = "https://github.com/X1nto/Mauth/issues"
29 | ),
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/account/AccountScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.account
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | @Immutable
6 | sealed interface AccountScreenState {
7 |
8 | @Immutable
9 | data object Loading : AccountScreenState
10 |
11 | @Immutable
12 | data class Success(val form: AccountForm) : AccountScreenState
13 |
14 | @Immutable
15 | data class Error(val error: String) : AccountScreenState
16 |
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/account/AccountViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.account
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.xinto.mauth.domain.account.AccountRepository
7 | import com.xinto.mauth.domain.account.model.DomainAccountInfo
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import kotlinx.coroutines.flow.asStateFlow
10 | import kotlinx.coroutines.flow.catch
11 | import kotlinx.coroutines.flow.launchIn
12 | import kotlinx.coroutines.flow.onEach
13 | import kotlinx.coroutines.launch
14 | import java.util.UUID
15 |
16 | sealed interface AccountViewModelParams {
17 | @JvmInline
18 | value class Id(val id: UUID) : AccountViewModelParams
19 |
20 | @JvmInline
21 | value class Prefilled(val accountInfo: DomainAccountInfo) : AccountViewModelParams
22 | }
23 |
24 | class AccountViewModel(
25 | application: Application,
26 |
27 | params: AccountViewModelParams,
28 | private val accounts: AccountRepository
29 | ) : AndroidViewModel(application) {
30 |
31 | private val _initialInfo = MutableStateFlow(null)
32 |
33 | private val _state = MutableStateFlow(AccountScreenState.Loading)
34 | val state = _state.asStateFlow()
35 |
36 | init {
37 | when (params) {
38 | is AccountViewModelParams.Id -> {
39 | accounts.getAccountInfo(params.id)
40 | .onEach {
41 | _initialInfo.value = it
42 | _state.value = AccountScreenState.Success(AccountForm(it))
43 | }.catch {
44 | _state.value = AccountScreenState.Error(it.localizedMessage ?: it.message ?: it.stackTraceToString())
45 | }.launchIn(viewModelScope)
46 | }
47 | is AccountViewModelParams.Prefilled -> {
48 | _initialInfo.value = params.accountInfo
49 | _state.value = AccountScreenState.Success(AccountForm(params.accountInfo))
50 | }
51 | }
52 | }
53 |
54 | fun saveData(accountInfo: DomainAccountInfo) {
55 | viewModelScope.launch {
56 | accounts.putAccount(accountInfo)
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/account/component/AccountComboBox.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.account.component
2 |
3 | import androidx.compose.animation.core.animateFloatAsState
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.material3.DropdownMenuItem
6 | import androidx.compose.material3.ExposedDropdownMenuBox
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.OutlinedTextField
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.draw.rotate
16 | import androidx.compose.ui.res.painterResource
17 | import com.xinto.mauth.R
18 | import kotlin.enums.EnumEntries
19 |
20 | @Composable
21 | fun > AccountComboBox(
22 | values: EnumEntries,
23 | value: E,
24 | onValueChange: (E) -> Unit,
25 | label: (@Composable () -> Unit)? = null
26 | ) {
27 | val (expanded, setExpanded) = remember {
28 | mutableStateOf(false)
29 | }
30 | ExposedDropdownMenuBox(
31 | expanded = expanded,
32 | onExpandedChange = setExpanded
33 | ) {
34 | OutlinedTextField(
35 | modifier = Modifier.fillMaxWidth().menuAnchor(),
36 | value = value.name,
37 | onValueChange = {},
38 | singleLine = true,
39 | label = label,
40 | readOnly = true,
41 | trailingIcon = {
42 | val iconRotation by animateFloatAsState(if (expanded) 180f else 0f)
43 | Icon(
44 | modifier = Modifier.rotate(iconRotation),
45 | painter = painterResource(R.drawable.ic_keyboard_arrow_down),
46 | contentDescription = null
47 | )
48 | }
49 | )
50 | ExposedDropdownMenu(
51 | expanded = expanded,
52 | onDismissRequest = { setExpanded(false) }
53 | ) {
54 | values.forEach {
55 | DropdownMenuItem(
56 | text = { Text(it.name) },
57 | onClick = {
58 | setExpanded(false)
59 | onValueChange(it)
60 | }
61 | )
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/account/component/AccountDataField.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.account.component
2 |
3 | import androidx.compose.foundation.text.KeyboardOptions
4 | import androidx.compose.material3.OutlinedTextField
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.res.stringResource
8 | import androidx.compose.ui.text.input.VisualTransformation
9 | import com.xinto.mauth.R
10 |
11 | @Composable
12 | fun AccountDataField(
13 | value: String,
14 | onValueChange: (String) -> Unit,
15 | required: Boolean = false,
16 | label: (@Composable () -> Unit)? = null,
17 | leadingIcon: (@Composable () -> Unit)? = null,
18 | trailingIcon: (@Composable () -> Unit)? = null,
19 | visualTransformation: VisualTransformation = VisualTransformation.None,
20 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
21 | ) {
22 | OutlinedTextField(
23 | value = value,
24 | onValueChange = onValueChange,
25 | singleLine = true,
26 | label = label,
27 | leadingIcon = leadingIcon,
28 | trailingIcon = trailingIcon,
29 | supportingText = if (required) { ->
30 | Text(stringResource(R.string.account_data_status_required))
31 | } else null,
32 | visualTransformation = visualTransformation,
33 | keyboardOptions = keyboardOptions,
34 | )
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/account/component/AccountExitDialog.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.account.component
2 |
3 | import androidx.compose.material3.AlertDialog
4 | import androidx.compose.material3.Text
5 | import androidx.compose.material3.TextButton
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.res.stringResource
8 | import com.xinto.mauth.R
9 |
10 | @Composable
11 | fun AccountExitDialog(
12 | onCancel: () -> Unit,
13 | onConfirm: () -> Unit,
14 | ) {
15 | AlertDialog(
16 | onDismissRequest = onCancel,
17 | title = {
18 | Text(stringResource(R.string.account_discard_title))
19 | },
20 | text = {
21 | Text(stringResource(R.string.account_discard_subtitle))
22 | },
23 | confirmButton = {
24 | TextButton(onClick = onConfirm) {
25 | Text(stringResource(R.string.account_discard_buttons_discard))
26 | }
27 | },
28 | dismissButton = {
29 | TextButton(onClick = onCancel) {
30 | Text(stringResource(R.string.account_discard_buttons_cancel))
31 | }
32 | }
33 | )
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/account/component/AccountNumberField.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.account.component
2 |
3 | import androidx.compose.foundation.text.KeyboardOptions
4 | import androidx.compose.material3.OutlinedTextField
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.text.input.KeyboardType
8 |
9 | @Composable
10 | fun AccountNumberField(
11 | value: String,
12 | onValueChange: (String) -> Unit,
13 | label: (@Composable () -> Unit)? = null,
14 | supportingText: (@Composable () -> Unit)? = null,
15 | min: Int = 0,
16 | max: Int = Int.MAX_VALUE
17 | ) {
18 | OutlinedTextField(
19 | value = value,
20 | onValueChange = onValueChange,
21 | singleLine = true,
22 | keyboardOptions = remember {
23 | KeyboardOptions(keyboardType = KeyboardType.Number)
24 | },
25 | supportingText = supportingText,
26 | label = label,
27 | isError = value.toIntOrNull() == null || (value.toInt() < min || value.toInt() > max)
28 | )
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/account/state/AccountScreenError.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.account.state
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.painterResource
12 | import androidx.compose.ui.res.stringResource
13 | import androidx.compose.ui.unit.dp
14 | import com.xinto.mauth.R
15 |
16 | @Composable
17 | fun AccountScreenError() {
18 | Column(
19 | modifier = Modifier.fillMaxSize(),
20 | verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
21 | horizontalAlignment = Alignment.CenterHorizontally
22 | ) {
23 | Icon(
24 | painter = painterResource(R.drawable.ic_error),
25 | contentDescription = null
26 | )
27 | Text(stringResource(R.string.account_error))
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/account/state/AccountScreenLoading.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.account.state
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.material3.CircularProgressIndicator
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 |
10 | @Composable
11 | fun AccountScreenLoading() {
12 | Box(
13 | modifier = Modifier.fillMaxSize(),
14 | contentAlignment = Alignment.Center
15 | ) {
16 | CircularProgressIndicator()
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/account/state/AccountScreenSuccess.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.account.state
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.lazy.grid.GridCells
7 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.unit.dp
11 | import com.xinto.mauth.ui.component.form.form
12 | import com.xinto.mauth.ui.screen.account.AccountForm
13 |
14 | @Composable
15 | fun AccountScreenSuccess(form: AccountForm) {
16 | LazyVerticalGrid(
17 | modifier = Modifier.fillMaxSize(),
18 | verticalArrangement = Arrangement.spacedBy(8.dp),
19 | horizontalArrangement = Arrangement.spacedBy(8.dp),
20 | contentPadding = PaddingValues(16.dp),
21 | columns = GridCells.Fixed(2)
22 | ) {
23 | form(form)
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/auth/AuthViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.auth
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.xinto.mauth.domain.AuthRepository
6 | import com.xinto.mauth.domain.SettingsRepository
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.asStateFlow
10 | import kotlinx.coroutines.flow.stateIn
11 | import kotlinx.coroutines.flow.update
12 |
13 | class AuthViewModel(
14 | private val authRepository: AuthRepository,
15 | private val settingsRepository: SettingsRepository
16 | ) : ViewModel() {
17 |
18 | private val _code = MutableStateFlow("")
19 | val code = _code.asStateFlow()
20 |
21 | val useBiometrics = settingsRepository.getUseBiometrics()
22 | .stateIn(
23 | scope = viewModelScope,
24 | started = SharingStarted.WhileSubscribed(5000),
25 | initialValue = false
26 | )
27 |
28 | fun insertNumber(number: Char) {
29 | _code.update { it + number }
30 | }
31 |
32 | fun deleteNumber() {
33 | _code.update { it.dropLast(1) }
34 | }
35 |
36 | fun clear() {
37 | _code.value = ""
38 | }
39 |
40 | suspend fun validate(code: String): Boolean {
41 | return authRepository.validate(code)
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/export/ExportScreen.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.export
2 |
3 | import androidx.activity.compose.BackHandler
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.IconButton
9 | import androidx.compose.material3.LargeTopAppBar
10 | import androidx.compose.material3.Scaffold
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.res.painterResource
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
19 | import com.xinto.mauth.R
20 | import com.xinto.mauth.domain.account.model.DomainExportAccount
21 | import com.xinto.mauth.ui.screen.export.state.ExportScreenError
22 | import com.xinto.mauth.ui.screen.export.state.ExportScreenLoading
23 | import com.xinto.mauth.ui.screen.export.state.ExportScreenSuccess
24 | import org.koin.androidx.compose.koinViewModel
25 | import org.koin.core.parameter.parametersOf
26 | import java.util.UUID
27 |
28 | @Composable
29 | fun ExportScreen(
30 | onBackNavigate: () -> Unit,
31 | accounts: List
32 | ) {
33 | BackHandler(onBack = onBackNavigate)
34 | val viewModel: ExportViewModel = koinViewModel {
35 | parametersOf(accounts)
36 | }
37 | val state by viewModel.state.collectAsStateWithLifecycle()
38 | ExportScreen(
39 | onBackNavigate = onBackNavigate,
40 | onCopyUrlToClipboard = {
41 | viewModel.copyUrlToClipboard(
42 | label = it.label,
43 | url = it.url
44 | )
45 | },
46 | state = state
47 | )
48 | }
49 |
50 | @Composable
51 | fun ExportScreen(
52 | onBackNavigate: () -> Unit,
53 | onCopyUrlToClipboard: (DomainExportAccount) -> Unit,
54 | state: ExportScreenState
55 | ) {
56 | Scaffold(
57 | topBar = {
58 | LargeTopAppBar(
59 | title = {
60 | Text(text = stringResource(R.string.export_title))
61 | },
62 | navigationIcon = {
63 | IconButton(onClick = onBackNavigate) {
64 | Icon(
65 | painter = painterResource(id = R.drawable.ic_arrow_back),
66 | contentDescription = null
67 | )
68 | }
69 | }
70 | )
71 | }
72 | ) { paddingValues ->
73 | Box(
74 | modifier = Modifier
75 | .fillMaxSize()
76 | .padding(paddingValues),
77 | contentAlignment = Alignment.Center
78 | ) {
79 | when (state) {
80 | is ExportScreenState.Loading -> {
81 | ExportScreenLoading()
82 | }
83 | is ExportScreenState.Success -> {
84 | ExportScreenSuccess(
85 | accounts = state.accounts,
86 | onCopyUrlToClipboard = onCopyUrlToClipboard
87 | )
88 | }
89 | is ExportScreenState.Error -> {
90 | ExportScreenError()
91 | }
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/export/ExportScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.export
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.xinto.mauth.domain.account.model.DomainExportAccount
5 |
6 | @Immutable
7 | sealed interface ExportScreenState {
8 |
9 | @Immutable
10 | data object Loading : ExportScreenState
11 |
12 | @Immutable
13 | data class Success(val accounts: List) : ExportScreenState
14 |
15 | @Immutable
16 | data object Error : ExportScreenState
17 |
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/export/ExportViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.export
2 |
3 | import android.app.Application
4 | import android.content.ClipData
5 | import android.content.ClipboardManager
6 | import android.os.Build
7 | import android.os.PersistableBundle
8 | import android.widget.Toast
9 | import androidx.core.content.getSystemService
10 | import androidx.lifecycle.AndroidViewModel
11 | import androidx.lifecycle.viewModelScope
12 | import com.xinto.mauth.Mauth
13 | import com.xinto.mauth.R
14 | import com.xinto.mauth.domain.account.AccountRepository
15 | import com.xinto.mauth.util.catchMap
16 | import kotlinx.coroutines.flow.SharingStarted
17 | import kotlinx.coroutines.flow.map
18 | import kotlinx.coroutines.flow.stateIn
19 | import java.util.UUID
20 |
21 | class ExportViewModel(
22 | application: Application,
23 |
24 | private val accounts: List,
25 | private val accountRepository: AccountRepository,
26 | ) : AndroidViewModel(application) {
27 |
28 | val state = accountRepository.getAccounts()
29 | .map { accounts ->
30 | val filteredAccounts = if (this.accounts.isEmpty()) {
31 | accounts
32 | } else {
33 | accounts.filter { this.accounts.contains(it.id) }
34 | }
35 |
36 | val exportAccounts = filteredAccounts.map {
37 | with (accountRepository) {
38 | it.toExportAccount()
39 | }
40 | }
41 | ExportScreenState.Success(exportAccounts)
42 | }.catchMap {
43 | ExportScreenState.Error
44 | }.stateIn(
45 | scope = viewModelScope,
46 | started = SharingStarted.WhileSubscribed(5000),
47 | initialValue = ExportScreenState.Loading
48 | )
49 |
50 | fun copyUrlToClipboard(label: String, url: String) {
51 | val application = getApplication()
52 | val clipboardService = application.getSystemService() ?: return
53 | val clipData = ClipData.newPlainText(label, url).apply {
54 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
55 | description.extras = PersistableBundle().apply {
56 | putBoolean("android.content.extra.IS_SENSITIVE", true)
57 | }
58 | }
59 | }
60 | clipboardService.setPrimaryClip(clipData)
61 | Toast.makeText(application, R.string.export_url_copy_success, Toast.LENGTH_LONG).show()
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/export/component/ZxingQrImage.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.export.component
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.aspectRatio
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material3.CircularProgressIndicator
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.LaunchedEffect
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.graphics.ImageBitmap
18 | import androidx.compose.ui.graphics.asImageBitmap
19 | import androidx.compose.ui.graphics.toArgb
20 | import androidx.compose.ui.layout.ContentScale
21 | import com.xinto.mauth.core.camera.ZxingEncoder
22 |
23 | @Composable
24 | fun ZxingQrImage(
25 | data: String,
26 | modifier: Modifier = Modifier,
27 | backgroundColor: Color = Color.White,
28 | contentColor: Color = Color.Black,
29 | contentScale: ContentScale = ContentScale.Fit
30 | ) {
31 | var bitmap by remember { mutableStateOf(null) }
32 | LaunchedEffect(data) {
33 | bitmap = null
34 | bitmap = ZxingEncoder.encodeToBitmap(
35 | data = data,
36 | size = 300,
37 | backgroundColor = backgroundColor.toArgb(),
38 | dataColor = contentColor.toArgb()
39 | ).asImageBitmap()
40 | }
41 | Box(modifier = modifier) {
42 | if (bitmap != null) {
43 | Image(
44 | modifier = Modifier
45 | .aspectRatio(1f)
46 | .fillMaxSize(),
47 | bitmap = bitmap!!,
48 | contentDescription = null,
49 | contentScale = contentScale
50 | )
51 | } else {
52 | Box(
53 | modifier = Modifier
54 | .aspectRatio(1f)
55 | .fillMaxSize(),
56 | contentAlignment = Alignment.Center
57 | ) {
58 | CircularProgressIndicator()
59 | }
60 | }
61 | }
62 | }
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/export/state/ExportScreenError.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.export.state
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.painterResource
11 | import androidx.compose.ui.unit.dp
12 | import com.xinto.mauth.R
13 |
14 | @Composable
15 | fun ExportScreenError() {
16 | Column(
17 | modifier = Modifier.fillMaxSize(),
18 | verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
19 | horizontalAlignment = Alignment.CenterHorizontally
20 | ) {
21 | Icon(
22 | painter = painterResource(R.drawable.ic_error),
23 | contentDescription = null
24 | )
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/export/state/ExportScreenLoading.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.export.state
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.material3.CircularProgressIndicator
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 |
10 | @Composable
11 | fun ExportScreenLoading() {
12 | Box(
13 | modifier = Modifier.fillMaxSize(),
14 | contentAlignment = Alignment.Center
15 | ) {
16 | CircularProgressIndicator()
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/home/HomeAddAccountMenu.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.home
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.StringRes
5 | import com.xinto.mauth.R
6 |
7 | enum class HomeAddAccountMenu(
8 | @DrawableRes val icon: Int,
9 | @StringRes val title: Int
10 | ) {
11 | ScanQR(R.drawable.ic_qr_code_scanner, R.string.home_addaccount_data_scanqr),
12 | ImageQR(R.drawable.ic_qr_code_2, R.string.home_addaccount_data_imageqr),
13 | Manual(R.drawable.ic_password, R.string.home_addaccount_data_manual)
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/home/HomeMoreMenu.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.home
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.StringRes
5 | import com.xinto.mauth.R
6 |
7 | enum class HomeMoreMenu(
8 | @DrawableRes val icon: Int,
9 | @StringRes val title: Int
10 | ) {
11 | Settings(R.drawable.ic_settings, R.string.settings_title),
12 | Export(R.drawable.ic_export, R.string.export_title),
13 | About(R.drawable.ic_info, R.string.about_title)
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/home/HomeScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.home
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.xinto.mauth.domain.account.model.DomainAccount
5 | import kotlinx.collections.immutable.ImmutableList
6 |
7 | @Immutable
8 | sealed interface HomeScreenState {
9 |
10 | @Immutable
11 | data object Loading : HomeScreenState
12 |
13 | @Immutable
14 | data object Empty : HomeScreenState
15 |
16 | @Immutable
17 | @JvmInline
18 | value class Success(val accounts: ImmutableList) : HomeScreenState
19 |
20 | @Immutable
21 | @JvmInline
22 | value class Error(val error: String) : HomeScreenState
23 |
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/home/component/HomeDeleteAccountsDialog.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.home.component
2 |
3 | import androidx.compose.material3.AlertDialog
4 | import androidx.compose.material3.FilledTonalButton
5 | import androidx.compose.material3.Icon
6 | import androidx.compose.material3.Text
7 | import androidx.compose.material3.TextButton
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.res.painterResource
10 | import androidx.compose.ui.res.stringResource
11 | import com.xinto.mauth.R
12 |
13 | @Composable
14 | fun HomeDeleteAccountsDialog(
15 | onConfirm: () -> Unit,
16 | onCancel: () -> Unit,
17 | ) {
18 | AlertDialog(
19 | onDismissRequest = onCancel,
20 | icon = {
21 | Icon(
22 | painter = painterResource(R.drawable.ic_delete_forever),
23 | contentDescription = null
24 | )
25 | },
26 | title = {
27 | Text(stringResource(R.string.home_delete_title))
28 | },
29 | text = {
30 | Text(stringResource(R.string.home_delete_subtitle))
31 | },
32 | confirmButton = {
33 | FilledTonalButton(onClick = onConfirm) {
34 | Text(stringResource(R.string.home_delete_button_delete))
35 | }
36 | },
37 | dismissButton = {
38 | TextButton(onClick = onCancel) {
39 | Text(stringResource(R.string.home_delete_button_cancel))
40 | }
41 | }
42 | )
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/home/state/HomeScreenEmpty.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.home.state
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.size
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.ProvideTextStyle
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.res.painterResource
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.dp
16 | import com.xinto.mauth.R
17 |
18 | @Composable
19 | fun HomeScreenEmpty(modifier: Modifier = Modifier) {
20 | Column(
21 | modifier = modifier,
22 | verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
23 | horizontalAlignment = Alignment.CenterHorizontally
24 | ) {
25 | Icon(
26 | modifier = Modifier.size(72.dp),
27 | painter = painterResource(R.drawable.ic_empty_dashboard),
28 | contentDescription = null
29 | )
30 | ProvideTextStyle(MaterialTheme.typography.headlineSmall) {
31 | Text(stringResource(R.string.home_dashboard_empty))
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/home/state/HomeScreenError.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.home.state
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.material3.Icon
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.painterResource
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.dp
13 | import com.xinto.mauth.R
14 |
15 | @Composable
16 | fun HomeScreenError(modifier: Modifier = Modifier) {
17 | Column(
18 | modifier = modifier,
19 | verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
20 | horizontalAlignment = Alignment.CenterHorizontally
21 | ) {
22 | Icon(
23 | painter = painterResource(R.drawable.ic_error),
24 | contentDescription = null
25 | )
26 | Text(stringResource(R.string.home_dashboard_error))
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/home/state/HomeScreenLoading.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.home.state
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.material3.CircularProgressIndicator
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 |
9 | @Composable
10 | fun HomeScreenLoading(modifier: Modifier = Modifier) {
11 | Box(
12 | modifier = modifier,
13 | contentAlignment = Alignment.Center
14 | ) {
15 | CircularProgressIndicator()
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/home/state/HomeScreenSuccess.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.home.state
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.lazy.grid.GridCells
6 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
7 | import androidx.compose.foundation.lazy.grid.items
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.snapshots.SnapshotStateList
10 | import androidx.compose.runtime.snapshots.SnapshotStateMap
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.unit.dp
13 | import com.xinto.mauth.domain.account.model.DomainAccount
14 | import com.xinto.mauth.domain.otp.model.DomainOtpRealtimeData
15 | import com.xinto.mauth.ui.screen.home.component.HomeAccountCard
16 | import kotlinx.collections.immutable.ImmutableList
17 | import java.util.UUID
18 |
19 | @Composable
20 | fun HomeScreenSuccess(
21 | modifier: Modifier = Modifier,
22 | onAccountSelect: (UUID) -> Unit,
23 | onAccountEdit: (UUID) -> Unit,
24 | onAccountCounterIncrease: (UUID) -> Unit,
25 | onAccountCopyCode: (String, String, Boolean) -> Unit,
26 | accounts: ImmutableList,
27 | selectedAccounts: SnapshotStateList,
28 | accountRealtimeData: SnapshotStateMap,
29 | ) {
30 | LazyVerticalGrid(
31 | modifier = modifier,
32 | contentPadding = PaddingValues(16.dp),
33 | verticalArrangement = Arrangement.spacedBy(12.dp),
34 | horizontalArrangement = Arrangement.spacedBy(12.dp),
35 | columns = GridCells.Adaptive(minSize = 250.dp),
36 | ) {
37 | items(
38 | items = accounts,
39 | key = { it.id }
40 | ) { account ->
41 | val realtimeData = accountRealtimeData[account.id]
42 | if (realtimeData != null) {
43 | HomeAccountCard(
44 | onClick = {
45 | if (selectedAccounts.isNotEmpty()) {
46 | onAccountSelect(account.id)
47 | }
48 | },
49 | onLongClick = {
50 | onAccountSelect(account.id)
51 | },
52 | onEdit = {
53 | onAccountEdit(account.id)
54 | },
55 | onCounterClick = {
56 | onAccountCounterIncrease(account.id)
57 | },
58 | onCopyCode = {
59 | onAccountCopyCode(account.label, realtimeData.code, it)
60 | },
61 | account = account,
62 | realtimeData = realtimeData,
63 | selected = selectedAccounts.contains(account.id)
64 | )
65 | }
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/pinremove/PinRemoveScreen.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.pinremove
2 |
3 | import android.content.res.Configuration
4 | import androidx.activity.compose.BackHandler
5 | import androidx.compose.material3.Icon
6 | import androidx.compose.material3.IconButton
7 | import androidx.compose.material3.LargeTopAppBar
8 | import androidx.compose.material3.Text
9 | import androidx.compose.material3.TopAppBar
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.ui.platform.LocalConfiguration
13 | import androidx.compose.ui.res.painterResource
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
16 | import com.xinto.mauth.R
17 | import com.xinto.mauth.ui.component.pinboard.PinScaffold
18 | import com.xinto.mauth.ui.component.pinboard.rememberPinBoardState
19 | import org.koin.androidx.compose.getViewModel
20 |
21 | @Composable
22 | fun PinRemoveScreen(
23 | onExit: () -> Unit
24 | ) {
25 | val viewModel: PinRemoveViewModel = getViewModel()
26 | val state by viewModel.state.collectAsStateWithLifecycle()
27 | BackHandler(onBack = onExit)
28 | PinRemoveScreen(
29 | state = state,
30 | onEnter = {
31 | if (viewModel.removePin()) {
32 | onExit()
33 | }
34 | },
35 | onBack = onExit,
36 | onNumberEnter = viewModel::addNumber,
37 | onNumberDelete = viewModel::deleteLast,
38 | onAllDelete = viewModel::clear
39 | )
40 | }
41 |
42 | @Composable
43 | fun PinRemoveScreen(
44 | state: PinRemoveScreenState,
45 | onEnter: () -> Unit,
46 | onBack: () -> Unit,
47 | onNumberEnter: (Char) -> Unit,
48 | onNumberDelete: () -> Unit,
49 | onAllDelete: () -> Unit,
50 | ) {
51 | val orientation = LocalConfiguration.current.orientation
52 | PinScaffold(
53 | codeLength = state.code.length,
54 | error = state is PinRemoveScreenState.Error,
55 | topBar = {
56 | if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
57 | TopAppBar(
58 | title = { AppBarTitle() },
59 | navigationIcon = { AppBarNavigationIcon(onBack) }
60 | )
61 | } else {
62 | LargeTopAppBar(
63 | title = { AppBarTitle() },
64 | navigationIcon = { AppBarNavigationIcon(onBack) }
65 | )
66 | }
67 | },
68 | description = null,
69 | useSmallButtons = orientation == Configuration.ORIENTATION_LANDSCAPE,
70 | state = rememberPinBoardState(
71 | showEnter = true,
72 | onNumberClick = onNumberEnter,
73 | onBackspaceClick = onNumberDelete,
74 | onEnterClick = onEnter,
75 | onBackspaceLongClick = onAllDelete
76 | )
77 | )
78 | }
79 |
80 | @Composable
81 | fun AppBarTitle() {
82 | Text(stringResource(R.string.pinremove_title))
83 | }
84 |
85 | @Composable
86 | fun AppBarNavigationIcon(onBack: () -> Unit) {
87 | IconButton(onClick = onBack) {
88 | Icon(
89 | painter = painterResource(R.drawable.ic_arrow_back),
90 | contentDescription = null
91 | )
92 | }
93 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/pinremove/PinRemoveScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.pinremove
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | @Immutable
6 | sealed interface PinRemoveScreenState {
7 | val code: String
8 |
9 | @Immutable
10 | @JvmInline
11 | value class Stale(override val code: String) : PinRemoveScreenState
12 |
13 | @Immutable
14 | data object Error : PinRemoveScreenState {
15 | override val code: String = ""
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/pinremove/PinRemoveViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.pinremove
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.xinto.mauth.domain.AuthRepository
5 | import com.xinto.mauth.domain.SettingsRepository
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.asStateFlow
8 | import kotlinx.coroutines.flow.update
9 | import kotlinx.coroutines.runBlocking
10 |
11 | class PinRemoveViewModel(
12 | private val authRepository: AuthRepository,
13 | private val settingsRepository: SettingsRepository
14 | ) : ViewModel() {
15 |
16 | private val _state = MutableStateFlow(PinRemoveScreenState.Stale(""))
17 | val state = _state.asStateFlow()
18 |
19 | /**
20 | * @return true if the screen should exit
21 | */
22 | fun removePin(): Boolean {
23 | return state.value.let {
24 | runBlocking {
25 | authRepository.validate(it.code).also { valid ->
26 | if (valid) {
27 | authRepository.removeCode()
28 | settingsRepository.setUseBiometrics(false)
29 | }
30 | }
31 | }.also { valid ->
32 | if (!valid) {
33 | _state.value = PinRemoveScreenState.Error
34 | }
35 | }
36 | }
37 | }
38 |
39 | fun addNumber(number: Char) {
40 | _state.update {
41 | when (it) {
42 | is PinRemoveScreenState.Stale -> PinRemoveScreenState.Stale(it.code + number)
43 | is PinRemoveScreenState.Error -> PinRemoveScreenState.Stale(number.toString())
44 | }
45 | }
46 | }
47 |
48 | fun deleteLast() {
49 | _state.update {
50 | if (it is PinRemoveScreenState.Stale) {
51 | PinRemoveScreenState.Stale(it.code.dropLast(1))
52 | } else it
53 | }
54 | }
55 |
56 | fun clear() {
57 | _state.value = PinRemoveScreenState.Stale("")
58 | }
59 |
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/pinsetup/PinSetupScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.pinsetup
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | @Immutable
6 | sealed interface PinSetupScreenState {
7 | @Immutable
8 | data object Initial : PinSetupScreenState
9 |
10 | @Immutable
11 | data object Confirm : PinSetupScreenState
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/pinsetup/PinSetupViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.pinsetup
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.xinto.mauth.domain.AuthRepository
5 | import kotlinx.coroutines.flow.MutableStateFlow
6 | import kotlinx.coroutines.flow.asStateFlow
7 | import kotlinx.coroutines.flow.update
8 |
9 | class PinSetupViewModel(
10 | private val authRepository: AuthRepository
11 | ) : ViewModel() {
12 |
13 | private var initialCode: String? = null
14 |
15 | private val _error = MutableStateFlow(false)
16 | val error = _error.asStateFlow()
17 |
18 | private val _code = MutableStateFlow("")
19 | val code = _code.asStateFlow()
20 |
21 | private val _state = MutableStateFlow(PinSetupScreenState.Initial)
22 | val state = _state.asStateFlow()
23 |
24 | /**
25 | * @return true if the screen should exit
26 | */
27 | fun next(): Boolean {
28 | if (_code.value.isEmpty()) {
29 | _error.value = true
30 | return false
31 | }
32 |
33 | if (state.value is PinSetupScreenState.Confirm) {
34 | val matches = initialCode == code.value
35 | if (matches) {
36 | authRepository.updateCode(code.value)
37 | } else {
38 | _error.value = true
39 | clear()
40 | }
41 | return matches
42 | }
43 |
44 | _state.value = PinSetupScreenState.Confirm
45 | _code.update {
46 | initialCode = it
47 | ""
48 | }
49 | return false
50 | }
51 |
52 | /**
53 | * @return true if the screen should exit
54 | */
55 | fun previous(): Boolean {
56 | if (state.value is PinSetupScreenState.Initial) {
57 | return true
58 | }
59 |
60 | clear()
61 | _state.value = PinSetupScreenState.Initial
62 | return false
63 | }
64 |
65 | fun addNumber(number: Char) {
66 | _error.value = false
67 | _code.update { it + number }
68 | }
69 |
70 | fun deleteLast() {
71 | _code.update { it.dropLast(1) }
72 | }
73 |
74 | fun clear() {
75 | _code.value = ""
76 | }
77 |
78 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/qrscan/QrScanViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.qrscan
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.xinto.mauth.domain.account.model.DomainAccountInfo
5 | import com.xinto.mauth.domain.otp.OtpRepository
6 |
7 | class QrScanViewModel(
8 | private val repository: OtpRepository
9 | ) : ViewModel() {
10 |
11 | fun parseResult(result: com.google.zxing.Result): DomainAccountInfo? {
12 | return repository.parseUriToAccountInfo(result.text)
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/qrscan/component/QrScanCamera.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.qrscan.component
2 |
3 | import android.content.Context
4 | import android.view.ViewGroup
5 | import androidx.camera.core.CameraSelector
6 | import androidx.camera.core.ImageAnalysis
7 | import androidx.camera.core.Preview
8 | import androidx.camera.lifecycle.ProcessCameraProvider
9 | import androidx.camera.view.PreviewView
10 | import androidx.compose.foundation.layout.Box
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.DisposableEffect
13 | import androidx.compose.runtime.Stable
14 | import androidx.compose.runtime.remember
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.platform.LocalContext
17 | import androidx.compose.ui.platform.LocalLifecycleOwner
18 | import androidx.compose.ui.viewinterop.AndroidView
19 |
20 | @Stable
21 | data class CameraState(
22 | val cameraProvider: ProcessCameraProvider,
23 | val analysis: ImageAnalysis,
24 | val preview: Preview,
25 | val cameraSelector: CameraSelector
26 | )
27 |
28 | @Composable
29 | fun rememberCameraState(
30 | context: Context,
31 | analysis: ImageAnalysis = ImageAnalysis.Builder().build(),
32 | preview: Preview = Preview.Builder().build(),
33 | cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
34 | ): CameraState {
35 | return remember(context, analysis, preview, cameraSelector) {
36 | CameraState(
37 | cameraProvider = ProcessCameraProvider.getInstance(context).get(),
38 | analysis = analysis,
39 | preview = preview,
40 | cameraSelector = cameraSelector
41 | )
42 | }
43 | }
44 |
45 | @Composable
46 | fun QrScanCamera(
47 | modifier: Modifier = Modifier,
48 | state: CameraState = rememberCameraState(LocalContext.current),
49 | ) {
50 | val lifecycleOwner = LocalLifecycleOwner.current
51 | DisposableEffect(state, lifecycleOwner) {
52 | state.cameraProvider.unbindAll()
53 | state.cameraProvider.bindToLifecycle(
54 | lifecycleOwner,
55 | state.cameraSelector,
56 | state.preview,
57 | state.analysis
58 | )
59 |
60 | onDispose {
61 | state.cameraProvider.unbindAll()
62 | }
63 | }
64 |
65 | Box(modifier = modifier) {
66 | AndroidView(
67 | factory = { context ->
68 | PreviewView(context).apply {
69 | scaleType = PreviewView.ScaleType.FILL_CENTER
70 | layoutParams = ViewGroup.LayoutParams(
71 | ViewGroup.LayoutParams.MATCH_PARENT,
72 | ViewGroup.LayoutParams.MATCH_PARENT,
73 | )
74 | implementationMode = PreviewView.ImplementationMode.COMPATIBLE
75 | }
76 | },
77 | update = { previewView ->
78 | state.preview.setSurfaceProvider(previewView.surfaceProvider)
79 | }
80 | )
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/qrscan/component/QrScanPermissionDeniedDialog.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.qrscan.component
2 |
3 | import androidx.compose.material3.AlertDialog
4 | import androidx.compose.material3.Button
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.res.stringResource
8 | import com.xinto.mauth.R
9 |
10 | @Composable
11 | fun QrScanPermissionDeniedDialog(
12 | shouldShowRationale: Boolean,
13 | onGrantPermission: () -> Unit,
14 | onCancel: () -> Unit,
15 | ) {
16 | AlertDialog(
17 | onDismissRequest = onCancel,
18 | text = {
19 | if (shouldShowRationale) {
20 | Text(stringResource(R.string.qrscan_permissions_subtitle_rationale))
21 | } else {
22 | Text(stringResource(R.string.qrscan_permissions_subtitle))
23 | }
24 | },
25 | confirmButton = {
26 | Button(onClick = onGrantPermission) {
27 | Text(stringResource(R.string.qrscan_permissions_button_grant))
28 | }
29 | },
30 | dismissButton = {
31 | Button(onClick = onCancel) {
32 | Text(stringResource(R.string.qrscan_permissions_button_cancel))
33 | }
34 | }
35 | )
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/qrscan/state/QrScanPermissionDenied.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.qrscan.state
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.painterResource
12 | import androidx.compose.ui.res.stringResource
13 | import androidx.compose.ui.unit.dp
14 | import com.xinto.mauth.R
15 |
16 | @Composable
17 | fun QrScanPermissionDenied() {
18 | Column(
19 | modifier = Modifier.fillMaxSize(),
20 | verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
21 | horizontalAlignment = Alignment.CenterHorizontally
22 | ) {
23 | Icon(
24 | painter = painterResource(R.drawable.ic_error),
25 | contentDescription = null
26 | )
27 | Text(stringResource(R.string.qrscan_error))
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/qrscan/state/QrScanPermissionGranted.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.qrscan.state
2 |
3 | import androidx.camera.core.ImageAnalysis
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.aspectRatio
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Surface
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.draw.clip
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.unit.dp
16 | import androidx.core.content.ContextCompat
17 | import com.xinto.mauth.core.camera.QrCodeAnalyzer
18 | import com.xinto.mauth.ui.screen.qrscan.component.QrScanCamera
19 | import com.xinto.mauth.ui.screen.qrscan.component.rememberCameraState
20 |
21 | @Composable
22 | fun QrScanPermissionGranted(
23 | onScan: (com.google.zxing.Result) -> Unit
24 | ) {
25 | Surface(
26 | modifier = Modifier
27 | .padding(32.dp)
28 | .aspectRatio(1f / 1f),
29 | shape = MaterialTheme.shapes.large,
30 | tonalElevation = 1.dp
31 | ) {
32 | Box(contentAlignment = Alignment.Center) {
33 | val context = LocalContext.current
34 | val cameraAnalysis = remember(context) {
35 | ImageAnalysis.Builder()
36 | .build()
37 | .also { analysis ->
38 | analysis.setAnalyzer(
39 | ContextCompat.getMainExecutor(context),
40 | QrCodeAnalyzer(
41 | onSuccess = onScan,
42 | onFail = {}
43 | )
44 | )
45 | }
46 | }
47 | QrScanCamera(
48 | modifier = Modifier
49 | .matchParentSize()
50 | .padding(12.dp)
51 | .clip(MaterialTheme.shapes.large),
52 | state = rememberCameraState(context, analysis = cameraAnalysis)
53 | )
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.settings
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.xinto.mauth.domain.AuthRepository
6 | import com.xinto.mauth.domain.SettingsRepository
7 | import kotlinx.coroutines.flow.SharingStarted
8 | import kotlinx.coroutines.flow.stateIn
9 | import kotlinx.coroutines.launch
10 |
11 | class SettingsViewModel(
12 | private val settings: SettingsRepository,
13 | private val authRepository: AuthRepository
14 | ) : ViewModel() {
15 |
16 | val secureMode = settings.getSecureMode()
17 | .stateIn(
18 | scope = viewModelScope,
19 | started = SharingStarted.WhileSubscribed(5000),
20 | initialValue = false
21 | )
22 |
23 | val pinLock = authRepository.observeIsProtected()
24 | .stateIn(
25 | scope = viewModelScope,
26 | started = SharingStarted.WhileSubscribed(5000),
27 | initialValue = false
28 | )
29 |
30 | val biometrics = settings.getUseBiometrics()
31 | .stateIn(
32 | scope = viewModelScope,
33 | started = SharingStarted.WhileSubscribed(5000),
34 | initialValue = false
35 | )
36 |
37 | fun updateSecureMode(newSecureMode: Boolean) {
38 | viewModelScope.launch {
39 | settings.setSecureMode(newSecureMode)
40 | }
41 | }
42 |
43 | fun toggleBiometrics() {
44 | viewModelScope.launch {
45 | settings.setUseBiometrics(!biometrics.value)
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/settings/component/SettingsItem.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.settings.component
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.material3.ListItemDefaults
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.unit.dp
9 | import com.xinto.mauth.ui.component.lazygroup.GroupedListItem
10 |
11 | @Composable
12 | fun SettingsItem(
13 | modifier: Modifier = Modifier,
14 | title: @Composable () -> Unit,
15 | description: (@Composable () -> Unit)? = null,
16 | trailing: (@Composable () -> Unit)? = null,
17 | icon: (@Composable () -> Unit)? = null,
18 | enabled: Boolean = true,
19 | ) {
20 | val colors = ListItemDefaults.colors(
21 | headlineColor = MaterialTheme.colorScheme.onSurface.let {
22 | if (!enabled) it.copy(alpha = 0.3f) else it
23 | },
24 | leadingIconColor = MaterialTheme.colorScheme.onSurface.let {
25 | if (!enabled) it.copy(alpha = 0.38f) else it
26 | },
27 | trailingIconColor = MaterialTheme.colorScheme.onSurface.let {
28 | if (!enabled) it.copy(alpha = 0.38f) else it
29 | },
30 | )
31 | GroupedListItem(
32 | modifier = modifier,
33 | leadingContent = icon,
34 | trailingContent = trailing,
35 | supportingContent = description,
36 | headlineContent = title,
37 | tonalElevation = 1.dp,
38 | colors = colors,
39 | )
40 | }
41 |
42 | @Composable
43 | fun SettingsItem(
44 | onClick: () -> Unit,
45 | modifier: Modifier = Modifier,
46 | title: @Composable () -> Unit,
47 | description: (@Composable () -> Unit)? = null,
48 | trailing: (@Composable () -> Unit)? = null,
49 | icon: (@Composable () -> Unit)? = null,
50 | enabled: Boolean = true,
51 | ) {
52 | SettingsItem(
53 | modifier = modifier
54 | .clickable(
55 | onClick = onClick,
56 | enabled = enabled
57 | ),
58 | icon = icon,
59 | description = description,
60 | title = title,
61 | trailing = trailing,
62 | enabled = enabled,
63 | )
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/settings/component/SettingsNavigateItem.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.settings.component
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.material3.Icon
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.res.painterResource
8 | import com.xinto.mauth.R
9 |
10 | @Composable
11 | fun SettingsNavigateItem(
12 | modifier: Modifier = Modifier,
13 | onClick: () -> Unit,
14 | title: @Composable () -> Unit,
15 | description: (@Composable () -> Unit)? = null,
16 | icon: (@Composable () -> Unit)? = null,
17 | enabled: Boolean = true,
18 | ) {
19 | SettingsItem(
20 | modifier = modifier
21 | .clickable(
22 | enabled = enabled,
23 | onClick = onClick
24 | ),
25 | icon = icon,
26 | description = description,
27 | title = title,
28 | trailing = {
29 | Icon(
30 | painter = painterResource(R.drawable.ic_navigate_next),
31 | contentDescription = null
32 | )
33 | },
34 | enabled = enabled
35 | )
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/settings/component/SettingsSwitchItem.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.settings.component
2 |
3 | import androidx.compose.foundation.selection.toggleable
4 | import androidx.compose.material3.Switch
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 |
8 | @Composable
9 | fun SettingsSwitchItem(
10 | modifier: Modifier = Modifier,
11 | onCheckedChange: ((Boolean) -> Unit)?,
12 | checked: Boolean,
13 | title: @Composable () -> Unit,
14 | description: (@Composable () -> Unit)? = null,
15 | icon: (@Composable () -> Unit)? = null,
16 | thumbContent: (@Composable () -> Unit)? = null,
17 | enabled: Boolean = true,
18 | ) {
19 | val toggleableModifier = if (onCheckedChange != null) {
20 | Modifier.toggleable(
21 | value = checked,
22 | enabled = enabled,
23 | onValueChange = onCheckedChange
24 | )
25 | } else Modifier
26 |
27 | SettingsItem(
28 | modifier = modifier
29 | .then(toggleableModifier),
30 | icon = icon,
31 | description = description,
32 | title = title,
33 | trailing = {
34 | Switch(
35 | checked = checked,
36 | onCheckedChange = onCheckedChange,
37 | enabled = enabled,
38 | thumbContent = thumbContent
39 | )
40 | },
41 | enabled = enabled
42 | )
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/screen/theme/ThemeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.screen.theme
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.xinto.mauth.core.settings.model.ColorSetting
6 | import com.xinto.mauth.core.settings.model.ThemeSetting
7 | import com.xinto.mauth.domain.SettingsRepository
8 | import kotlinx.coroutines.flow.SharingStarted
9 | import kotlinx.coroutines.flow.stateIn
10 | import kotlinx.coroutines.launch
11 |
12 | class ThemeViewModel(
13 | private val settingsRepository: SettingsRepository
14 | ) : ViewModel() {
15 |
16 | val theme = settingsRepository.getTheme()
17 | .stateIn(
18 | scope = viewModelScope,
19 | initialValue = ThemeSetting.DEFAULT,
20 | started = SharingStarted.WhileSubscribed(5000)
21 | )
22 |
23 | val color = settingsRepository.getColor()
24 | .stateIn(
25 | scope = viewModelScope,
26 | initialValue = ColorSetting.DEFAULT,
27 | started = SharingStarted.WhileSubscribed(5000)
28 | )
29 |
30 | fun updateTheme(newTheme: ThemeSetting) {
31 | viewModelScope.launch {
32 | settingsRepository.setTheme(newTheme)
33 | }
34 | }
35 |
36 | fun updateColor(newColor: ColorSetting) {
37 | viewModelScope.launch {
38 | settingsRepository.setColor(newColor)
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.dynamicDarkColorScheme
7 | import androidx.compose.material3.dynamicLightColorScheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.platform.LocalContext
10 | import androidx.compose.ui.platform.LocalInspectionMode
11 | import com.xinto.mauth.core.settings.model.ColorSetting
12 | import com.xinto.mauth.core.settings.model.ThemeSetting
13 | import com.xinto.mauth.ui.theme.color.BlueberryBlueDark
14 | import com.xinto.mauth.ui.theme.color.LimeGreenDark
15 | import com.xinto.mauth.ui.theme.color.MothPurpleDark
16 | import com.xinto.mauth.ui.theme.color.OrangeOrangeDark
17 | import com.xinto.mauth.ui.theme.color.SkyCyanDark
18 | import com.xinto.mauth.ui.theme.color.LemonYellowDark
19 | import com.xinto.mauth.ui.theme.color.BlueberryBlueLight
20 | import com.xinto.mauth.ui.theme.color.LemonYellowLight
21 | import com.xinto.mauth.ui.theme.color.LimeGreenLight
22 | import com.xinto.mauth.ui.theme.color.MothPurpleLight
23 | import com.xinto.mauth.ui.theme.color.OrangeOrangeLight
24 | import com.xinto.mauth.ui.theme.color.SkyCyanLight
25 |
26 | @Composable
27 | fun MauthTheme(
28 | theme: ThemeSetting = ThemeSetting.DEFAULT,
29 | color: ColorSetting = ColorSetting.DEFAULT,
30 | content: @Composable () -> Unit
31 | ) {
32 | val isDark = when (theme) {
33 | ThemeSetting.System -> isSystemInDarkTheme()
34 | ThemeSetting.Dark -> true
35 | ThemeSetting.Light -> false
36 | }
37 | val isInPreview = LocalInspectionMode.current
38 | val colorScheme = when {
39 | color == ColorSetting.Dynamic && (isInPreview || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
40 | val context = LocalContext.current
41 | when (isDark) {
42 | true -> dynamicDarkColorScheme(context)
43 | false -> dynamicLightColorScheme(context)
44 | }
45 | }
46 | color == ColorSetting.BlueberryBlue -> when (isDark) {
47 | true -> BlueberryBlueDark
48 | false -> BlueberryBlueLight
49 | }
50 | color == ColorSetting.PickleYellow -> when (isDark) {
51 | true -> LemonYellowDark
52 | false -> LemonYellowLight
53 | }
54 | color == ColorSetting.ToxicGreen -> when (isDark) {
55 | true -> LimeGreenDark
56 | false -> LimeGreenLight
57 | }
58 | color == ColorSetting.LeatherOrange -> when (isDark) {
59 | true -> OrangeOrangeDark
60 | false -> OrangeOrangeLight
61 | }
62 | color == ColorSetting.OceanTurquoise -> when (isDark) {
63 | true -> SkyCyanDark
64 | false -> SkyCyanLight
65 | }
66 | else -> when (isDark) {
67 | true -> MothPurpleDark
68 | false -> MothPurpleLight
69 | }
70 | }
71 | MaterialTheme(
72 | colorScheme = colorScheme,
73 | typography = Typography,
74 | content = content
75 | )
76 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 |
5 | val Typography = Typography()
--------------------------------------------------------------------------------
/app/src/main/java/com/xinto/mauth/util/Coroutines.kt:
--------------------------------------------------------------------------------
1 | package com.xinto.mauth.util
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.coroutineScope
5 | import androidx.lifecycle.flowWithLifecycle
6 | import kotlinx.coroutines.Job
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.catch
9 | import kotlinx.coroutines.flow.launchIn
10 | import kotlinx.coroutines.flow.onEach
11 |
12 | inline fun Flow.catchMap(
13 | crossinline transformation: (Throwable) -> T
14 | ): Flow {
15 | return catch {
16 | emit(transformation(it))
17 | }
18 | }
19 |
20 | fun Flow.launchInLifecycle(
21 | lifecycle: Lifecycle,
22 | state: Lifecycle.State = Lifecycle.State.STARTED,
23 | action: suspend (T) -> Unit
24 | ): Job {
25 | return flowWithLifecycle(lifecycle, state)
26 | .onEach(action)
27 | .launchIn(lifecycle.coroutineScope)
28 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add_a_photo.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_apartment.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_back.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_downward.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_upward.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_backspace.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_brush.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_bug.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_check.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_close.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_contrast.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_copy_all.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_delete_forever.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_edit.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_empty_dashboard.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_error.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_export.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_fingerprint.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_github.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_info.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_key.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_keyboard_arrow_down.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_label.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_moon.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_more_vert.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_navigate_next.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_password.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_qr_code_2.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_qr_code_scanner.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_settings.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_sort.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_sun.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_tab.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_undo.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_visibility.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_visibility_off.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-tr-rTR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Burada henüz bir şey yok
3 | Bir hesap ekle
4 | QR kodunu tarayın veya anahtarı manuel olarak girin
5 | QR kodunu tarayın
6 | Bir resim seçin
7 | Verileri manuel olarak girin
8 | Hesaplar silinsin mi?
9 | Seçilen hesaplar kalıcı olarak silinecek.
10 | Sil
11 | İptal
12 |
13 | QR kodunu tarayın
14 | Eksik izinler
15 | Kamera izni verilmedi.
16 | Bu uygulamanın QR kodunu taraması ve analiz etmesi için kamera izni gerekir, Kamera izni olmadan Mauth hesaplarınızı tarayamaz ve içe aktaramaz.
17 | İzin Ver
18 | İptal Et
19 |
20 | Bir hesap ekle
21 | Bir hesabı düzenleyin
22 | Kaydet
23 | Etiket
24 | Issuer
25 | Secret
26 | Type
27 | Algoritma
28 | Rakamlar
29 | Counter
30 | Period
31 | Değişiklikleri iptal Et
32 | Değişiklikleriniz kaydedilmeyecek.
33 | Discard
34 | İptal Et
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #420042
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application") version "8.7.2" apply false
3 | kotlin("android") version "2.0.21" apply false
4 | kotlin("plugin.compose") version "2.0.21" apply false
5 | id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
6 | }
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/10.txt:
--------------------------------------------------------------------------------
1 | v0.1.0:
2 | - Initial release!
3 | - TOTP Support
4 | - Manual account add support
5 | - Dynamic theming via Material You
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/20.txt:
--------------------------------------------------------------------------------
1 | v0.2.0:
2 | - Revamped the home screen.
3 | - Implemented individual timers for each account.
4 | - Issuers are now visible on the account cards.
5 | - Added icons to the account cards.
6 | - Finished the "Add Account" screen.
7 | - Added the QR code scanner.
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/21.txt:
--------------------------------------------------------------------------------
1 | v0.2.1:
2 | A hotfix release.
3 | - Prevent the camera from closing (freezing) when an invalid QR code is detected.
4 | - Fixed a crash that occurs when the URI label is not provided.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/30.txt:
--------------------------------------------------------------------------------
1 | v0.3.0:
2 | - Added an ability to select and delete accounts.
3 | - Added Norwegian Bokmål language.
4 | - Removed the placeholders from the text fields in the account add screen.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40.txt:
--------------------------------------------------------------------------------
1 | v0.4.0:
2 | - Add account editing.
3 | - Add HOTP support.
4 | - Fix dropdown menus in the account add/edit menu.
5 | - Fix the account icon not resetting after re-entering the account add/edit menu.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/50.txt:
--------------------------------------------------------------------------------
1 | v0.5.0
2 |
3 | New features
4 | ============
5 | - Added a settings page
6 | - Includes a "Secure Mode" switch that protects you from exposing data via screenshots
7 | - `otpauth://` deeplink support
8 | - Added a splash screen for devices running on Android 11 and below
9 | - Turkish translation
10 |
11 | Changes
12 | =======
13 | - Small UI updates
14 | - "Full-screen" immersive navigation and top bars
15 | - Animated code display
16 | - Remove placeholder icon buttons in home bottom bar
17 | - Updates to account card
18 | - 90% of the app was rewritten from scratch! This should result in a better performance and will make subsequent updates much easier
19 |
20 | Bug fixes
21 | =========
22 | - Fixed a bug where camera would not properly close after exiting the QR Scan screen
23 | - Fixed many possible crashes
24 |
25 | *Note: due to changes to the app's database, a complex migration was written to convert from the old schema to the new one. Please report issues if Mauth starts crashing or accounts disappear after updating to v0.5.0*
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/51.txt:
--------------------------------------------------------------------------------
1 | v0.5.1
2 |
3 | Changes
4 | =======
5 | - Updated the account screen
6 | - The number fields will now turn red when an incorrect number is entered
7 | - Required fields are now marked
8 | - The save button will be greyed out until you satisfy all the requirements
9 |
10 | Fixes
11 | =====
12 | - Fixed a crash when editing number fields in the account screen
13 | - Fixed a crash when saving the edited account in the account screen
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/52.txt:
--------------------------------------------------------------------------------
1 | v0.5.2
2 |
3 | Changes
4 | =======
5 | - Implement form validation for number fields in the account screen
6 | - Fields are now coerced to a specific range
7 | - Digits field's ranges are annotated
8 |
9 | Fixes
10 | =====
11 | - Fixed a bug where Mauth would accept any value for number fields in the Account screen, rendering the app unusable
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/53.txt:
--------------------------------------------------------------------------------
1 | v0.5.3
2 |
3 | Fixes
4 | =====
5 | - Fixed the form validation in the account screen. Again!
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/60.txt:
--------------------------------------------------------------------------------
1 | v0.6.0
2 |
3 | New Features
4 | ============
5 | - You can now sort your accounts by label, issuer and addition date (both ascending and descending)
6 | - The app's icon now supports Material You colors
7 | - The accounts now store their addition/creation date
8 |
9 | Fixes
10 | =====
11 | - Fixed a bug where Android's clipboard preview would show the account code, even if it was hidden
12 | - Updated the Add Account bottom sheet for consistency with the Material 3 guidelines
13 | - Minor bottom bar menu fixes
14 | - Refactored and optimized some parts of the code
15 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/61.txt:
--------------------------------------------------------------------------------
1 | v0.6.1
2 |
3 | Fixes
4 | =====
5 | - Fixed the parsing for OTP strings that had lowercase algorithm names
6 | - The buttons in the "Add Account" bottom sheet no longer overlap with the system navigation bar
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/70.txt:
--------------------------------------------------------------------------------
1 | New Features
2 | ============
3 | - Added Chinese (Simplified) translation
4 | - Added Portuguese translation
5 | - Implemented authentication methods
6 | - PIN Authentication
7 | - Biometric Authentication (requires PIN auth to be enabled)
8 | - Added the About screen
9 | - The About screen includes a shortcut that navigates you to the repository's feedback (issues) page
10 |
11 | Changes
12 | =======
13 | - Updated navigation transitions to be more fluid
14 | - Other small UI tweaks
15 |
16 | Note: Please consider reporting any bugs regarding the new pin and biometric authentication on Mauth's feedback page. The more security issues I fix before v1.0.0, the safer it'll be for people to use Mauth.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/80.txt:
--------------------------------------------------------------------------------
1 | New Features
2 | ============
3 | - Added theme settings, allowing you to customize how Mauth looks like
4 |
5 | Changes
6 | =========
7 | - Updated pt-BR translation
8 | - Updated Chinese (Simplified) translation
9 | - The "Add account" buttons now have the same color
10 | - Rephrased the sorting option labels
11 |
12 | Fixes
13 | =====
14 | - Fixed auth pin button colors and behavior
15 | - Fixed a bug that allowed empty PINs to be set
16 | - The edit button should no longer disappear when the label is too long
17 | - More fixes to the buttons in the "Add account" sheet
18 | - Improved the OTP URI parser
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/90.txt:
--------------------------------------------------------------------------------
1 | New Features
2 | ============
3 | - Added the export screen. It's possible to either export all accounts from the 3-dot menu, or export the selected accounts only from the selection menu. Exporting requires authentication if you have PIN lock enabled.
4 | - Added initial support for tablets and landscape mode.
5 | - Added Russian translation
6 |
7 | Changes
8 | =======
9 | - Updated Norwegian Bokmål translation
10 | - Updated Chinese (Simplified) translation
11 | - Updated theme colors
12 | - Custom icons are copied to app's internal directory
13 |
14 | Fixes
15 | =====
16 | - Fixed a blank screen appearing after scanning a QR code
17 | - Fixed custom icons not saving properly
18 | - Fixed performance issues with the home and account add screens
19 | - Other minor UI fixes
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Mauth (pronounced Moth) is a Two-Factor Authentication app with support for TOTP and HOTP and compatibility with Google Authenticator. It comes with a rich UI while also providing many necessary features.
2 |
3 | Features:
4 | - Compatible with Google Authenticator
5 | - Secure
6 | - Many ways to add your accounts
7 | - Algorithms
8 | - Organization
9 | - Export/Import (coming soon)
10 |
11 | Mauth uses latest Android technology (Jetpack Compose, CameraX, Biometrics, Room DB and more) to provide the best user experience with the least amount of bugs possible. It is in an active development but is nowhere near ready for daily usage. Use it at your own risk.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | A Material You Two-factor Authentication app.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Mauth
--------------------------------------------------------------------------------
/fastlane/metadata/android/ru/full_description.txt:
--------------------------------------------------------------------------------
1 | Mauth (произносится как моз) это двухфакторное приложение для аутентификации с поддержкой TOTP и HOTP и совместимое с Google Authenticator. Оно имеет красивый интерфейс, а также предоставляет много необходимых функций.
2 |
3 | Возможности:
4 | - Совместимо с Google Authenticator
5 | - Безопасное
6 | - Множество способов добавить свои учетные записи
7 | - Алгоритмы
8 | - Организация
9 | - Экспорт/импорт
10 |
11 | Mauth использует новейшие технологии Android (JetPack Compose, Camerax, Biometrics, Room DB и другие) чтобы обеспечить наилучший пользовательский опыт с наименьшим количеством ошибок. Оно находится в активной разработке, но ещё не готово к ежедневному использованию. Используйте его на свой страх и риск.
--------------------------------------------------------------------------------
/fastlane/metadata/android/ru/short_description.txt:
--------------------------------------------------------------------------------
1 | Приложение для двухфакторной аутентификации в стиле Material You
--------------------------------------------------------------------------------
/github/get_it_on_github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/github/get_it_on_github.png
--------------------------------------------------------------------------------
/github/mauth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/github/mauth.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed May 31 13:57:10 GET 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven(url = "https://jitpack.io")
14 | }
15 | }
16 | rootProject.name = "Mauth"
17 | include(":app")
18 |
--------------------------------------------------------------------------------