├── .github
├── dependabot.yaml
└── workflows
│ └── android.yaml
├── .gitignore
├── LICENSE
├── README.md
├── app
├── build.gradle.kts
├── proguard-rules.pro
├── schemas
│ └── dev.sanmer.authenticator.database.AppDatabase
│ │ ├── 1.json
│ │ └── 2.json
└── src
│ ├── debug
│ └── res
│ │ └── drawable
│ │ ├── launcher_foreground.xml
│ │ └── launcher_splash.xml
│ └── main
│ ├── AndroidManifest.xml
│ ├── kotlin
│ └── dev
│ │ └── sanmer
│ │ ├── authenticator
│ │ ├── App.kt
│ │ ├── Const.kt
│ │ ├── compat
│ │ │ └── PermissionCompat.kt
│ │ ├── database
│ │ │ ├── AppDatabase.kt
│ │ │ ├── dao
│ │ │ │ └── TotpDao.kt
│ │ │ └── entity
│ │ │ │ └── TotpEntity.kt
│ │ ├── datastore
│ │ │ ├── PreferenceSerializer.kt
│ │ │ └── model
│ │ │ │ ├── Ntp.kt
│ │ │ │ └── Preference.kt
│ │ ├── ktx
│ │ │ ├── ContextExt.kt
│ │ │ ├── LocaleExt.kt
│ │ │ ├── LocaleListCompatExt.kt
│ │ │ └── StringExt.kt
│ │ ├── model
│ │ │ ├── AuthType.kt
│ │ │ ├── impl
│ │ │ │ └── TotpImpl.kt
│ │ │ └── serializer
│ │ │ │ ├── AuthJson.kt
│ │ │ │ ├── AuthTxt.kt
│ │ │ │ └── TotpAuth.kt
│ │ ├── repository
│ │ │ ├── DbRepository.kt
│ │ │ ├── PreferenceRepository.kt
│ │ │ ├── SecureRepository.kt
│ │ │ └── TimeRepository.kt
│ │ ├── ui
│ │ │ ├── AuthorizeActivity.kt
│ │ │ ├── CryptoActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── component
│ │ │ │ ├── DropdownMenu.kt
│ │ │ │ ├── LabelText.kt
│ │ │ │ ├── Logo.kt
│ │ │ │ ├── PageIndicator.kt
│ │ │ │ └── Swipe.kt
│ │ │ ├── ktx
│ │ │ │ ├── ClipboardExt.kt
│ │ │ │ ├── LazyListStateExt.kt
│ │ │ │ ├── ModifierExt.kt
│ │ │ │ ├── NavControllerExt.kt
│ │ │ │ ├── PaddingValuesExt.kt
│ │ │ │ └── ShapeExt.kt
│ │ │ ├── main
│ │ │ │ ├── LockScreen.kt
│ │ │ │ └── MainScreen.kt
│ │ │ ├── provider
│ │ │ │ └── LocalPreference.kt
│ │ │ ├── screens
│ │ │ │ ├── authorize
│ │ │ │ │ ├── AuthorizeScreen.kt
│ │ │ │ │ └── component
│ │ │ │ │ │ ├── ChangePasswordItem.kt
│ │ │ │ │ │ ├── EnterPasswordItem.kt
│ │ │ │ │ │ └── PasswordTextField.kt
│ │ │ │ ├── crypto
│ │ │ │ │ └── CryptoScreen.kt
│ │ │ │ ├── edit
│ │ │ │ │ ├── EditScreen.kt
│ │ │ │ │ └── component
│ │ │ │ │ │ ├── DigitsAndPeriodItem.kt
│ │ │ │ │ │ ├── SecretItem.kt
│ │ │ │ │ │ ├── TextFieldContent.kt
│ │ │ │ │ │ ├── TextFieldItem.kt
│ │ │ │ │ │ └── TypeAndHashItem.kt
│ │ │ │ ├── encode
│ │ │ │ │ └── EncodeScreen.kt
│ │ │ │ ├── home
│ │ │ │ │ ├── HomeScreen.kt
│ │ │ │ │ └── component
│ │ │ │ │ │ ├── AuthItem.kt
│ │ │ │ │ │ └── AuthList.kt
│ │ │ │ ├── ntp
│ │ │ │ │ ├── NtpScreen.kt
│ │ │ │ │ └── component
│ │ │ │ │ │ ├── NtpItem.kt
│ │ │ │ │ │ └── NtpList.kt
│ │ │ │ ├── scan
│ │ │ │ │ └── ScanScreen.kt
│ │ │ │ ├── security
│ │ │ │ │ ├── SecurityScreen.kt
│ │ │ │ │ └── component
│ │ │ │ │ │ └── SecurityItem.kt
│ │ │ │ ├── settings
│ │ │ │ │ ├── SettingsScreen.kt
│ │ │ │ │ └── component
│ │ │ │ │ │ ├── DatabaseItem.kt
│ │ │ │ │ │ ├── PreferenceItem.kt
│ │ │ │ │ │ ├── SettingItem.kt
│ │ │ │ │ │ ├── TokenItem.kt
│ │ │ │ │ │ └── ToolItem.kt
│ │ │ │ └── trash
│ │ │ │ │ ├── TrashScreen.kt
│ │ │ │ │ └── component
│ │ │ │ │ ├── AuthItem.kt
│ │ │ │ │ └── AuthList.kt
│ │ │ └── theme
│ │ │ │ ├── Shape.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ └── viewmodel
│ │ │ ├── AuthorizeViewModel.kt
│ │ │ ├── CryptoViewModel.kt
│ │ │ ├── EditViewModel.kt
│ │ │ ├── EncodeViewModel.kt
│ │ │ ├── HomeViewModel.kt
│ │ │ ├── MainViewModel.kt
│ │ │ ├── NtpViewModel.kt
│ │ │ ├── ScanViewModel.kt
│ │ │ ├── SettingsViewModel.kt
│ │ │ └── TrashViewModel.kt
│ │ └── crypto
│ │ └── BiometricKey.kt
│ └── res
│ ├── drawable
│ ├── a_b.xml
│ ├── alert_triangle.xml
│ ├── arrow_bar_down.xml
│ ├── arrow_bar_up.xml
│ ├── arrow_left.xml
│ ├── brand_github_2.xml
│ ├── camera_off.xml
│ ├── caret_down.xml
│ ├── caret_up.xml
│ ├── check.xml
│ ├── circle_check_filled.xml
│ ├── clipboard_text.xml
│ ├── database.xml
│ ├── database_export.xml
│ ├── database_import.xml
│ ├── device_floppy.xml
│ ├── edit.xml
│ ├── eye.xml
│ ├── eye_closed.xml
│ ├── file_export.xml
│ ├── file_import.xml
│ ├── fingerprint.xml
│ ├── key.xml
│ ├── launcher_foreground.xml
│ ├── launcher_splash.xml
│ ├── lock.xml
│ ├── lock_off.xml
│ ├── lock_open.xml
│ ├── math_function.xml
│ ├── mood_heart.xml
│ ├── pencil_plus.xml
│ ├── photo.xml
│ ├── qrcode.xml
│ ├── refresh.xml
│ ├── restore.xml
│ ├── scan.xml
│ ├── search.xml
│ ├── settings_2.xml
│ ├── shield.xml
│ ├── shield_off.xml
│ ├── timezone.xml
│ ├── tool.xml
│ ├── trash.xml
│ ├── trash_x.xml
│ ├── user.xml
│ └── x.xml
│ ├── mipmap-anydpi-v26
│ └── launcher.xml
│ ├── values-night-v31
│ └── colors.xml
│ ├── values-night
│ ├── colors.xml
│ └── themes.xml
│ ├── values-v31
│ └── colors.xml
│ ├── values-zh-rCN
│ └── strings.xml
│ ├── values
│ ├── colors.xml
│ ├── colors_material.xml
│ ├── strings.xml
│ ├── strings_untranslatable.xml
│ └── themes.xml
│ └── xml
│ └── locales_config.xml
├── build-logic
├── build.gradle.kts
├── settings.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── ApplicationConventionPlugin.kt
│ ├── ComposeConventionPlugin.kt
│ ├── HiltConventionPlugin.kt
│ ├── LibraryConventionPlugin.kt
│ ├── ProjectExt.kt
│ └── RoomConventionPlugin.kt
├── build.gradle.kts
├── core
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── dev
│ └── sanmer
│ ├── crypto
│ ├── Crypto.kt
│ ├── PasswordKey.kt
│ └── SessionKey.kt
│ ├── encoding
│ ├── Base32.kt
│ └── Base64.kt
│ ├── ntp
│ └── NtpServer.kt
│ ├── otp
│ ├── ByteArray.kt
│ ├── HOTP.kt
│ ├── OtpUri.kt
│ └── TOTP.kt
│ └── qrcode
│ └── QRCode.kt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── logo
├── afphoto
│ ├── aliyun.afphoto
│ ├── apple.afphoto
│ ├── aws.afphoto
│ ├── azure.afphoto
│ ├── binance.afphoto
│ ├── bybit.afphoto
│ ├── cloudflare.afphoto
│ ├── coinbase.afphoto
│ ├── crowdin.afphoto
│ ├── digitalocean.afphoto
│ ├── discord.afphoto
│ ├── facebook.afphoto
│ ├── gitea.afphoto
│ ├── github.afphoto
│ ├── gitlab.afphoto
│ ├── google.afphoto
│ ├── instagram.afphoto
│ ├── jetbrains.afphoto
│ ├── lark.afphoto
│ ├── mega.afphoto
│ ├── meta.afphoto
│ ├── microsoft.afphoto
│ ├── okx.afphoto
│ ├── onlyfans.afphoto
│ ├── openai.afphoto
│ ├── orcid.afphoto
│ ├── patreon.afphoto
│ ├── paypal.afphoto
│ ├── pixiv.afphoto
│ ├── tencent-cloud.afphoto
│ ├── wise.afphoto
│ └── x.afphoto
├── build.gradle.kts
├── src
│ └── main
│ │ ├── kotlin
│ │ └── dev
│ │ │ └── sanmer
│ │ │ └── logo
│ │ │ ├── Brand.kt
│ │ │ ├── Logo.kt
│ │ │ └── Normal.kt
│ │ └── res
│ │ ├── drawable-night
│ │ ├── brand_apple.xml
│ │ ├── brand_aws.xml
│ │ ├── brand_bybit.xml
│ │ ├── brand_github.xml
│ │ ├── brand_okx.xml
│ │ ├── brand_openai.xml
│ │ ├── brand_patreon.xml
│ │ └── brand_x.xml
│ │ └── drawable
│ │ ├── brand_aliyun.xml
│ │ ├── brand_apple.xml
│ │ ├── brand_aws.xml
│ │ ├── brand_azure.xml
│ │ ├── brand_binance.xml
│ │ ├── brand_bybit.xml
│ │ ├── brand_cloudflare.xml
│ │ ├── brand_coinbase.xml
│ │ ├── brand_crowdin.xml
│ │ ├── brand_digitalocean.xml
│ │ ├── brand_discord.xml
│ │ ├── brand_facebook.xml
│ │ ├── brand_gitea.xml
│ │ ├── brand_github.xml
│ │ ├── brand_gitlab.xml
│ │ ├── brand_google.xml
│ │ ├── brand_instagram.xml
│ │ ├── brand_jetbrains.xml
│ │ ├── brand_lark.xml
│ │ ├── brand_mega.xml
│ │ ├── brand_meta.xml
│ │ ├── brand_microsoft.xml
│ │ ├── brand_okx.xml
│ │ ├── brand_onlyfans.xml
│ │ ├── brand_openai.xml
│ │ ├── brand_orcid.xml
│ │ ├── brand_patreon.xml
│ │ ├── brand_paypal.xml
│ │ ├── brand_pixiv.xml
│ │ ├── brand_tencent_cloud.xml
│ │ ├── brand_wise.xml
│ │ ├── brand_x.xml
│ │ ├── normal_cloud.xml
│ │ └── normal_default.xml
└── svg
│ ├── aliyun.svg
│ ├── apple.svg
│ ├── aws.svg
│ ├── azure.svg
│ ├── binance.svg
│ ├── bybit.svg
│ ├── cloudflare.svg
│ ├── coinbase.svg
│ ├── crowdin.svg
│ ├── digitalocean.svg
│ ├── discord.svg
│ ├── facebook.svg
│ ├── gitea.svg
│ ├── github.svg
│ ├── gitlab.svg
│ ├── google.svg
│ ├── instagram.svg
│ ├── jetbrains.svg
│ ├── lark.svg
│ ├── mega.svg
│ ├── meta.svg
│ ├── microsoft.svg
│ ├── okx.svg
│ ├── onlyfans.svg
│ ├── openai.svg
│ ├── orcid.svg
│ ├── patreon.svg
│ ├── paypal.svg
│ ├── pixiv.svg
│ ├── tencent-cloud.svg
│ ├── wise.svg
│ └── x.svg
└── settings.gradle.kts
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | time: "21:00"
8 | labels: [ "github_actions" ]
9 |
10 | - package-ecosystem: gradle
11 | directory: "/"
12 | schedule:
13 | interval: daily
14 | time: "21:00"
15 | labels: [ "dependencies" ]
16 | registries: "*"
17 | ignore:
18 | - dependency-name: "self.*"
19 | groups:
20 | kotlin-ksp:
21 | patterns:
22 | - "org.jetbrains.kotlin:*"
23 | - "org.jetbrains.kotlin.jvm"
24 | - "com.google.devtools.ksp"
25 | - "com.google.devtools.ksp.gradle.plugin"
26 |
27 | registries:
28 | maven-google:
29 | type: "maven-repository"
30 | url: "https://maven.google.com"
31 | replaces-base: true
32 |
--------------------------------------------------------------------------------
/.github/workflows/android.yaml:
--------------------------------------------------------------------------------
1 | name: Android
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0
18 |
19 | - name: Set up signing key
20 | if: github.ref == 'refs/heads/main'
21 | run: |
22 | if [ ! -z "${{ secrets.KEY_STORE }}" ]; then
23 | echo keyStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> signing.properties
24 | echo keyAlias='${{ secrets.KEY_ALIAS }}' >> signing.properties
25 | echo keyPassword='${{ secrets.KEY_PASSWORD }}' >> signing.properties
26 | echo keyStore='${{ github.workspace }}/key.jks' >> signing.properties
27 | echo ${{ secrets.KEY_STORE }} | base64 --decode > ${{ github.workspace }}/key.jks
28 | fi
29 |
30 | - name: Set up JDK
31 | uses: actions/setup-java@v4
32 | with:
33 | distribution: 'jetbrains'
34 | java-version: 21
35 |
36 | - name: Set up Gradle
37 | uses: gradle/actions/setup-gradle@v4
38 |
39 | - name: Build with Gradle
40 | run: ./gradlew assembleRelease
41 |
42 | - name: Get release name
43 | if: success() && github.ref == 'refs/heads/main'
44 | id: release-name
45 | run: |
46 | name=`ls app/build/outputs/apk/release/*.apk | awk -F '(/|.apk)' '{print $6}'` && echo "name=${name}" >> $GITHUB_OUTPUT
47 |
48 | - name: Upload apk
49 | if: success() && github.ref == 'refs/heads/main'
50 | uses: actions/upload-artifact@v4
51 | with:
52 | name: ${{ steps.release-name.outputs.name }}
53 | path: app/build/outputs/apk/release/*.apk*
54 |
55 | - name: Upload mapping
56 | if: success() && github.ref == 'refs/heads/main'
57 | uses: actions/upload-artifact@v4
58 | with:
59 | name: ${{ steps.release-name.outputs.name }}-mapping
60 | path: app/build/outputs/mapping/release
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle
2 | .gradle/
3 | build/
4 |
5 | # Kotlin
6 | .kotlin
7 |
8 | # Local configuration
9 | local.properties
10 | signing.properties
11 |
12 | # Android Studio
13 | captures/
14 | release/
15 | .externalNativeBuild/
16 | .cxx/
17 |
18 | # IntelliJ
19 | *.iml
20 | .idea/
21 |
22 | # Keystore
23 | *.jks
24 | *.keystore
25 |
26 | # MacOS
27 | .DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Authenticator
2 | [](https://github.com/SanmerApps/Authenticator/releases) [](https://github.com/SanmerApps/Authenticator/releases/latest)
3 |
4 | ## Supported Versions
5 | Android 13 ~ 15
6 |
7 | ## Credits
8 | - [tabler/tabler-icons](https://github.com/tabler/tabler-icons.git)
9 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -repackageclasses dev.sanmer.authenticator
2 |
--------------------------------------------------------------------------------
/app/src/debug/res/drawable/launcher_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/App.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import androidx.camera.camera2.Camera2Config
6 | import androidx.camera.core.CameraXConfig
7 | import dagger.hilt.android.HiltAndroidApp
8 | import timber.log.Timber
9 |
10 | @HiltAndroidApp
11 | class App : Application(), CameraXConfig.Provider {
12 | init {
13 | Timber.plant(Timber.DebugTree())
14 | }
15 |
16 | override fun getCameraXConfig() =
17 | CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
18 | .apply {
19 | if (BuildConfig.DEBUG) {
20 | setMinimumLoggingLevel(Log.DEBUG)
21 | } else {
22 | setMinimumLoggingLevel(Log.INFO)
23 | }
24 | }.build()
25 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/Const.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator
2 |
3 | object Const {
4 | const val GITHUB_URL = "https://github.com/SanmerApps/Authenticator"
5 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/compat/PermissionCompat.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.compat
2 |
3 | import android.content.Context
4 | import android.content.pm.PackageManager
5 | import androidx.activity.result.ActivityResultRegistryOwner
6 | import androidx.activity.result.contract.ActivityResultContracts
7 | import androidx.core.content.ContextCompat
8 | import dev.sanmer.authenticator.ktx.findActivity
9 | import java.util.UUID
10 |
11 | object PermissionCompat {
12 | @JvmInline
13 | value class PermissionState(
14 | private val state: Map
15 | ) {
16 | val allGranted get() = state.all { it.value }
17 | }
18 |
19 | fun checkPermissions(
20 | context: Context,
21 | permissions: List
22 | ) = PermissionState(
23 | permissions.associateWith {
24 | ContextCompat.checkSelfPermission(
25 | context, it
26 | ) == PackageManager.PERMISSION_GRANTED
27 | }
28 | )
29 |
30 | fun checkPermission(
31 | context: Context,
32 | permission: String
33 | ) = checkPermissions(
34 | context = context,
35 | permissions = listOf(permission)
36 | ).allGranted
37 |
38 | fun requestPermissions(
39 | context: Context,
40 | permissions: List,
41 | callback: (PermissionState) -> Unit = {}
42 | ) {
43 | val activity = context.findActivity()
44 | if (activity !is ActivityResultRegistryOwner) return
45 |
46 | val activityResultRegistry = activity.activityResultRegistry
47 | val launcher = activityResultRegistry.register(
48 | key = UUID.randomUUID().toString(),
49 | contract = ActivityResultContracts.RequestMultiplePermissions(),
50 | callback = { callback(PermissionState(it)) }
51 | )
52 |
53 | launcher.launch(permissions.toTypedArray())
54 | }
55 |
56 | fun requestPermission(
57 | context: Context,
58 | permission: String,
59 | callback: (Boolean) -> Unit = {}
60 | ) = requestPermissions(
61 | context = context,
62 | permissions = listOf(permission),
63 | callback = { callback(it.allGranted) }
64 | )
65 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/database/dao/TotpDao.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.database.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import androidx.room.Update
8 | import dev.sanmer.authenticator.database.entity.TotpEntity
9 | import kotlinx.coroutines.flow.Flow
10 |
11 | @Dao
12 | interface TotpDao {
13 | @Query("SELECT * FROM totp WHERE deletedAt != 0")
14 | suspend fun getAllTrashed(): List
15 |
16 | @Query("SELECT * FROM totp WHERE deletedAt = 0")
17 | fun getAllEnabledAsFlow(): Flow>
18 |
19 | @Query("SELECT * FROM totp WHERE deletedAt != 0")
20 | fun getAllTrashedAsFlow(): Flow>
21 |
22 | @Query("SELECT * FROM totp WHERE id = :id")
23 | fun getByIdAsFlow(id: Long): Flow
24 |
25 | @Query("SELECT * FROM totp")
26 | suspend fun getAll(): List
27 |
28 | @Insert
29 | suspend fun insert(entity: TotpEntity)
30 |
31 | @Insert
32 | suspend fun insert(entities: List)
33 |
34 | @Update
35 | suspend fun update(entity: TotpEntity)
36 |
37 | @Update
38 | suspend fun update(entities: List)
39 |
40 | @Delete
41 | suspend fun delete(entity: TotpEntity)
42 |
43 | @Delete
44 | suspend fun delete(entities: List)
45 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/database/entity/TotpEntity.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.database.entity
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import dev.sanmer.authenticator.model.serializer.TotpAuth
6 | import dev.sanmer.otp.HOTP
7 | import kotlin.time.Duration.Companion.days
8 | import kotlin.time.Duration.Companion.milliseconds
9 |
10 | @Entity(tableName = "totp")
11 | data class TotpEntity(
12 | @PrimaryKey(autoGenerate = true)
13 | val id: Long = 0,
14 | val deletedAt: Long = 0,
15 | val issuer: String,
16 | val name: String,
17 | val secret: String,
18 | val hash: HOTP.Hash,
19 | val digits: Int,
20 | val period: Long
21 | ) {
22 | val lifetime inline get() = (System.currentTimeMillis() - deletedAt).milliseconds
23 |
24 | val displayName inline get() = "$issuer ($name)"
25 |
26 | constructor(
27 | auth: TotpAuth
28 | ) : this(
29 | issuer = auth.issuer,
30 | name = auth.name,
31 | secret = auth.secret,
32 | hash = auth.hash,
33 | digits = auth.digits,
34 | period = auth.period
35 | )
36 |
37 | val totp get() = TotpAuth(
38 | issuer = issuer,
39 | name = name,
40 | secret = secret,
41 | hash = hash,
42 | digits = digits,
43 | period = period
44 | )
45 |
46 | companion object Default {
47 | val LIFETIME_MAX = 7.days
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/datastore/PreferenceSerializer.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.datastore
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.CorruptionException
5 | import androidx.datastore.core.DataStore
6 | import androidx.datastore.core.DataStoreFactory
7 | import androidx.datastore.core.Serializer
8 | import androidx.datastore.dataStoreFile
9 | import dagger.Module
10 | import dagger.Provides
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.android.qualifiers.ApplicationContext
13 | import dagger.hilt.components.SingletonComponent
14 | import dev.sanmer.authenticator.datastore.model.Preference
15 | import dev.sanmer.authenticator.ktx.deviceProtectedContext
16 | import kotlinx.serialization.SerializationException
17 | import java.io.InputStream
18 | import java.io.OutputStream
19 | import javax.inject.Inject
20 | import javax.inject.Singleton
21 |
22 | class PreferenceSerializer @Inject constructor() : Serializer {
23 | override val defaultValue = Preference()
24 |
25 | override suspend fun readFrom(input: InputStream) =
26 | try {
27 | Preference.decodeFromStream(input)
28 | } catch (e: SerializationException) {
29 | throw CorruptionException("Failed to read proto", e)
30 | }
31 |
32 | override suspend fun writeTo(t: Preference, output: OutputStream) {
33 | t.encodeToStream(output)
34 | }
35 |
36 | @Module
37 | @InstallIn(SingletonComponent::class)
38 | object Impl {
39 | @Provides
40 | @Singleton
41 | fun dataStore(
42 | @ApplicationContext context: Context,
43 | serializer: PreferenceSerializer
44 | ): DataStore =
45 | DataStoreFactory.create(
46 | serializer = serializer
47 | ) {
48 | context.deviceProtectedContext.dataStoreFile("preference.pb")
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/datastore/model/Ntp.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.datastore.model
2 |
3 | enum class Ntp {
4 | Custom,
5 | Alibaba,
6 | Apple,
7 | Amazon,
8 | Cloudflare,
9 | Google,
10 | Meta,
11 | Microsoft,
12 | Tencent;
13 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/datastore/model/Preference.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.datastore.model
2 |
3 | import kotlinx.serialization.Serializable
4 | import kotlinx.serialization.decodeFromByteArray
5 | import kotlinx.serialization.encodeToByteArray
6 | import kotlinx.serialization.protobuf.ProtoBuf
7 | import kotlinx.serialization.protobuf.ProtoNumber
8 | import java.io.InputStream
9 | import java.io.OutputStream
10 |
11 | @Serializable
12 | data class Preference(
13 | @ProtoNumber(1)
14 | val keyEncryptedByPassword: String = "",
15 | @ProtoNumber(2)
16 | val keyEncryptedByBiometric: String = "",
17 | @ProtoNumber(3)
18 | val ntpAddress: String = "",
19 | @ProtoNumber(4)
20 | val ntp: Ntp = Ntp.Cloudflare
21 | ) {
22 | val isEncrypted inline get() = keyEncryptedByPassword.isNotEmpty()
23 | val isBiometric inline get() = keyEncryptedByBiometric.isNotEmpty()
24 |
25 | fun encodeToStream(output: OutputStream) = output.write(
26 | ProtoBuf.encodeToByteArray(this)
27 | )
28 |
29 | companion object Default {
30 | fun decodeFromStream(input: InputStream): Preference =
31 | ProtoBuf.decodeFromByteArray(input.readBytes())
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ktx/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ktx
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.ContextWrapper
6 | import android.content.Intent
7 | import androidx.core.app.LocaleManagerCompat
8 | import java.util.Locale
9 |
10 | val Context.applicationLocale: Locale?
11 | inline get() = LocaleManagerCompat.getApplicationLocales(applicationContext)
12 | .toList().firstOrNull()
13 |
14 | val Context.deviceProtectedContext: Context
15 | inline get() = createDeviceProtectedStorageContext()
16 |
17 | fun Context.viewUrl(url: String) {
18 | startActivity(
19 | Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
20 | )
21 | }
22 |
23 | fun Context.findActivity(): Activity? {
24 | var context = this
25 | while (context is ContextWrapper) {
26 | if (context is Activity) return context
27 | context = context.baseContext
28 | }
29 |
30 | return null
31 | }
32 |
33 | fun Context.finishActivity() {
34 | if (this is Activity) finish()
35 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ktx/LocaleExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ktx
2 |
3 | import java.util.Locale
4 |
5 | val Locale.localizedDisplayName: String
6 | inline get() = getDisplayName(this)
7 | .replaceFirstChar {
8 | if (it.isLowerCase()) {
9 | it.titlecase(this)
10 | } else {
11 | it.toString()
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ktx/LocaleListCompatExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ktx
2 |
3 | import androidx.core.os.LocaleListCompat
4 | import java.util.Locale
5 |
6 | fun LocaleListCompat.toList(): List = List(size()) { this[it]!! }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ktx/StringExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ktx
2 |
3 | import kotlin.math.min
4 |
5 | fun String.hidden(limit: Int = 6, mask: Char = '\u2022'): String {
6 | val showLength = min(length / 2, limit)
7 | val hiddenLength = min(length - showLength, limit)
8 | val sb = StringBuilder()
9 | sb.appendRange(this, 0, showLength)
10 | repeat(hiddenLength) { sb.append(mask) }
11 | return sb.toString()
12 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/model/AuthType.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.model
2 |
3 | enum class AuthType {
4 | TOTP
5 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/model/impl/TotpImpl.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.model.impl
2 |
3 | import dev.sanmer.authenticator.database.entity.TotpEntity
4 | import dev.sanmer.encoding.decodeBase32
5 | import dev.sanmer.otp.HOTP
6 | import kotlinx.coroutines.flow.StateFlow
7 | import kotlinx.coroutines.flow.map
8 |
9 | data class TotpImpl(
10 | val entity: TotpEntity,
11 | val epochSeconds: StateFlow
12 | ) {
13 | val secret by lazy { entity.secret.decodeBase32() }
14 |
15 | val otp = epochSeconds.map {
16 | HOTP.otp(
17 | hash = entity.hash,
18 | secret = secret,
19 | digits = entity.digits,
20 | counter = it / entity.period
21 | )
22 | }
23 |
24 | fun now() = HOTP.otp(
25 | hash = entity.hash,
26 | secret = secret,
27 | digits = entity.digits,
28 | counter = epochSeconds.value / entity.period
29 | )
30 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/model/serializer/AuthJson.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.model.serializer
2 |
3 | import kotlinx.serialization.Serializable
4 | import kotlinx.serialization.json.Json
5 | import kotlinx.serialization.json.decodeFromStream
6 | import kotlinx.serialization.json.encodeToStream
7 | import java.io.InputStream
8 | import java.io.OutputStream
9 |
10 | @Serializable
11 | data class AuthJson(
12 | val totp: List
13 | ) {
14 | fun encodeTo(output: OutputStream) {
15 | endpointJson.encodeToStream(this, output)
16 | }
17 |
18 | companion object Default {
19 | private val endpointJson = Json {
20 | prettyPrint = true
21 | }
22 |
23 | const val MIME_TYPE = "application/json"
24 | const val FILE_NAME = "auth.json"
25 |
26 | fun decodeFrom(input: InputStream): AuthJson =
27 | Json.decodeFromStream(input)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/model/serializer/AuthTxt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.model.serializer
2 |
3 | import dev.sanmer.authenticator.model.AuthType
4 | import dev.sanmer.otp.HOTP
5 | import dev.sanmer.otp.OtpUri.Default.toOtpUri
6 | import java.io.InputStream
7 | import java.io.OutputStream
8 | import kotlin.streams.toList
9 |
10 | data class AuthTxt(
11 | val totp: List
12 | ) {
13 | fun encodeTo(output: OutputStream) {
14 | val uris = totp.map { it.uri.toString() }
15 | val content = uris.joinToString(separator = "\n")
16 | output.write(content.toByteArray())
17 | }
18 |
19 | companion object Default {
20 | const val MIME_TYPE = "text/plain"
21 | const val FILE_NAME = "auth.txt"
22 |
23 | fun parse(uriString: String): TotpAuth {
24 | val uri = uriString.toOtpUri()
25 | AuthType.valueOf(uri.type)
26 |
27 | return TotpAuth(
28 | issuer = uri.issuer,
29 | name = uri.name,
30 | secret = uri.secret,
31 | hash = uri.algorithm?.let(HOTP.Hash::valueOf) ?: HOTP.Hash.SHA1,
32 | digits = uri.digits ?: 6,
33 | period = uri.period ?: 30,
34 | )
35 | }
36 |
37 | fun decodeFrom(input: InputStream): AuthTxt {
38 | return AuthTxt(
39 | input.bufferedReader().lines().toList()
40 | .mapNotNull {
41 | runCatching {
42 | parse(it)
43 | }.getOrNull()
44 | }
45 | )
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/model/serializer/TotpAuth.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.model.serializer
2 |
3 | import dev.sanmer.authenticator.model.AuthType
4 | import dev.sanmer.otp.HOTP
5 | import dev.sanmer.otp.OtpUri
6 | import kotlinx.serialization.Serializable
7 |
8 | @Serializable
9 | data class TotpAuth(
10 | val issuer: String,
11 | val name: String,
12 | val secret: String,
13 | val hash: HOTP.Hash,
14 | val digits: Int,
15 | val period: Long
16 | ) {
17 | val uri get() = OtpUri(
18 | type = AuthType.TOTP.name,
19 | name = name,
20 | issuer = issuer,
21 | secret = secret,
22 | algorithm = hash.name,
23 | digits = digits,
24 | period = period
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/repository/PreferenceRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.repository
2 |
3 | import androidx.datastore.core.DataStore
4 | import dev.sanmer.authenticator.datastore.model.Ntp
5 | import dev.sanmer.authenticator.datastore.model.Preference
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.withContext
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | class PreferenceRepository @Inject constructor(
13 | private val dataStore: DataStore
14 | ) {
15 | val data get() = dataStore.data
16 |
17 | suspend fun setKeyEncryptedByPassword(value: String) {
18 | withContext(Dispatchers.IO) {
19 | dataStore.updateData {
20 | it.copy(keyEncryptedByPassword = value)
21 | }
22 | }
23 | }
24 |
25 | suspend fun setKeyEncryptedByBiometric(value: String) {
26 | withContext(Dispatchers.IO) {
27 | dataStore.updateData {
28 | it.copy(keyEncryptedByBiometric = value)
29 | }
30 | }
31 | }
32 |
33 | suspend fun setNtpAddress(value: String) {
34 | withContext(Dispatchers.IO) {
35 | dataStore.updateData {
36 | it.copy(ntpAddress = value)
37 | }
38 | }
39 | }
40 |
41 | suspend fun setNtp(value: Ntp) {
42 | withContext(Dispatchers.IO) {
43 | dataStore.updateData {
44 | it.copy(ntp = value)
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/repository/SecureRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.repository
2 |
3 | import dev.sanmer.authenticator.database.dao.TotpDao
4 | import dev.sanmer.crypto.Crypto
5 | import dev.sanmer.crypto.SessionKey
6 | import dev.sanmer.encoding.decodeBase64
7 | import dev.sanmer.encoding.encodeBase64
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.withContext
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Singleton
14 | class SecureRepository @Inject constructor(
15 | private val totp: TotpDao,
16 | ) {
17 | private var key: Crypto? = null
18 |
19 | fun setSessionKey(sessionKey: SessionKey?) {
20 | key = sessionKey
21 | }
22 |
23 | suspend fun encrypt(input: String) = key?.encrypt(input) ?: input
24 |
25 | suspend fun decrypt(input: String) = key?.decrypt(input) ?: input
26 |
27 | suspend fun encryptSecret(key: Crypto) = withContext(Dispatchers.IO) {
28 | totp.update(
29 | totp.getAll().map { it.copy(secret = key.encrypt(it.secret)) }
30 | )
31 | }
32 |
33 | suspend fun decryptSecret(key: Crypto) = withContext(Dispatchers.IO) {
34 | totp.update(
35 | totp.getAll().map { it.copy(secret = key.decrypt(it.secret)) }
36 | )
37 | }
38 |
39 | suspend fun encryptSecretByNewKey(current: Crypto, new: Crypto) = withContext(Dispatchers.IO) {
40 | totp.update(
41 | totp.getAll().map {
42 | val decrypted = current.decrypt(it.secret.decodeBase64())
43 | it.copy(secret = new.encrypt(decrypted).encodeBase64())
44 | }
45 | )
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/component/DropdownMenu.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.component
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.ColumnScope
5 | import androidx.compose.foundation.shape.CornerBasedShape
6 | import androidx.compose.material3.DropdownMenu
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.unit.DpOffset
12 | import androidx.compose.ui.window.PopupProperties
13 |
14 | @Composable
15 | fun DropdownMenu(
16 | expanded: Boolean,
17 | onDismissRequest: () -> Unit,
18 | modifier: Modifier = Modifier,
19 | shape: CornerBasedShape = MaterialTheme.shapes.small,
20 | contentAlignment: Alignment = Alignment.TopStart,
21 | offset: DpOffset = DpOffset.Zero,
22 | properties: PopupProperties = PopupProperties(focusable = true),
23 | surface: @Composable () -> Unit,
24 | content: @Composable ColumnScope.() -> Unit
25 | ) = Box(
26 | modifier = modifier
27 | ) {
28 | surface()
29 |
30 | Box(
31 | modifier = Modifier.align(contentAlignment),
32 | contentAlignment = contentAlignment
33 | ) {
34 | DropdownMenu(
35 | expanded = expanded,
36 | onDismissRequest = onDismissRequest,
37 | offset = offset,
38 | properties = properties,
39 | shape = shape,
40 | content = content
41 | )
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/component/LabelText.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.component
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.shape.CircleShape
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.text.TextStyle
12 | import androidx.compose.ui.unit.dp
13 |
14 | @Composable
15 | fun LabelText(
16 | text: String,
17 | modifier: Modifier = Modifier,
18 | style: TextStyle = MaterialTheme.typography.labelMedium,
19 | color: Color = MaterialTheme.colorScheme.onSecondaryContainer,
20 | containerColor: Color = MaterialTheme.colorScheme.secondaryContainer,
21 | ) = Text(
22 | text = text,
23 | style = style,
24 | color = color,
25 | modifier = modifier
26 | .background(
27 | color = containerColor,
28 | shape = CircleShape
29 | )
30 | .padding(horizontal = 8.dp, vertical = 2.dp)
31 | )
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/component/Logo.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.component
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.shape.CircleShape
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.LocalContentColor
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Surface
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.graphics.Shape
16 | import androidx.compose.ui.res.painterResource
17 |
18 | @Composable
19 | fun Logo(
20 | @DrawableRes icon: Int,
21 | modifier: Modifier = Modifier,
22 | shape: Shape = CircleShape,
23 | contentColor: Color = MaterialTheme.colorScheme.onPrimary,
24 | containerColor: Color = MaterialTheme.colorScheme.primary,
25 | fraction: Float = 0.6f
26 | ) = Surface(
27 | modifier = modifier,
28 | shape = shape,
29 | color = containerColor,
30 | contentColor = contentColor
31 | ) {
32 | Box(
33 | contentAlignment = Alignment.Center
34 | ) {
35 | Icon(
36 | modifier = Modifier.fillMaxSize(fraction),
37 | painter = painterResource(id = icon),
38 | contentDescription = null,
39 | tint = LocalContentColor.current
40 | )
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/ktx/ClipboardExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.ktx
2 |
3 | import android.content.ClipData
4 | import android.content.ClipDescription
5 | import android.os.PersistableBundle
6 | import androidx.compose.ui.platform.Clipboard
7 | import androidx.compose.ui.platform.toClipEntry
8 |
9 | suspend fun Clipboard.setSensitiveText(
10 | content: String
11 | ) {
12 | val data = ClipData.newPlainText("plain text", content).apply {
13 | description.extras = PersistableBundle().apply {
14 | putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
15 | }
16 | }
17 |
18 | setClipEntry(data.toClipEntry())
19 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/ktx/LazyListStateExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.ktx
2 |
3 | import androidx.compose.foundation.lazy.LazyListState
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.State
6 | import androidx.compose.runtime.derivedStateOf
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.mutableIntStateOf
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.runtime.setValue
11 |
12 | @Composable
13 | fun LazyListState.isScrollingUp(): State {
14 | var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) }
15 | var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) }
16 | return remember(this) {
17 | derivedStateOf {
18 | if (previousIndex != firstVisibleItemIndex) {
19 | previousIndex > firstVisibleItemIndex
20 | } else {
21 | previousScrollOffset >= firstVisibleItemScrollOffset
22 | }.also {
23 | previousIndex = firstVisibleItemIndex
24 | previousScrollOffset = firstVisibleItemScrollOffset
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/ktx/ModifierExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.ktx
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.border
6 | import androidx.compose.runtime.Stable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.draw.clip
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.Shape
11 |
12 | @Stable
13 | fun Modifier.surface(
14 | shape: Shape,
15 | backgroundColor: Color,
16 | border: BorderStroke? = null
17 | ) = then(if (border != null) Modifier.border(border = border, shape = shape) else Modifier)
18 | .background(color = backgroundColor, shape = shape)
19 | .clip(shape = shape)
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/ktx/NavControllerExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.ktx
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavOptionsBuilder
5 |
6 | fun NavController.navigateSingleTopTo(
7 | route: String,
8 | builder: NavOptionsBuilder.() -> Unit = {}
9 | ) = navigate(
10 | route = route
11 | ) {
12 | launchSingleTop = true
13 | restoreState = true
14 | builder()
15 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/ktx/PaddingValuesExt.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE")
2 |
3 | package dev.sanmer.authenticator.ui.ktx
4 |
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.ui.unit.Dp
7 | import androidx.compose.ui.unit.LayoutDirection
8 | import javax.annotation.concurrent.Immutable
9 |
10 | inline operator fun PaddingValues.plus(other: PaddingValues): PaddingValues =
11 | OperatorPaddingValues(this, other, Dp::plus)
12 |
13 | inline operator fun PaddingValues.minus(other: PaddingValues): PaddingValues =
14 | OperatorPaddingValues(this, other, Dp::minus)
15 |
16 | @Immutable
17 | class OperatorPaddingValues(
18 | private val that: PaddingValues,
19 | private val other: PaddingValues,
20 | private val operator: Dp.(Dp) -> Dp,
21 | ) : PaddingValues {
22 | override fun calculateBottomPadding(): Dp =
23 | operator(
24 | that.calculateBottomPadding(),
25 | other.calculateBottomPadding()
26 | )
27 |
28 | override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp =
29 | operator(
30 | that.calculateLeftPadding(layoutDirection),
31 | other.calculateLeftPadding(layoutDirection)
32 | )
33 |
34 | override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp =
35 | operator(
36 | that.calculateRightPadding(layoutDirection),
37 | other.calculateRightPadding(layoutDirection)
38 | )
39 |
40 | override fun calculateTopPadding(): Dp =
41 | operator(
42 | that.calculateTopPadding(),
43 | other.calculateTopPadding()
44 | )
45 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/ktx/ShapeExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.ktx
2 |
3 | import androidx.compose.foundation.shape.CornerBasedShape
4 | import androidx.compose.foundation.shape.CornerSize
5 | import androidx.compose.ui.unit.Dp
6 |
7 | fun CornerBasedShape.bottom(size: Dp) =
8 | copy(bottomStart = CornerSize(size), bottomEnd = CornerSize(size))
9 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/provider/LocalPreference.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.provider
2 |
3 | import androidx.compose.runtime.staticCompositionLocalOf
4 | import dev.sanmer.authenticator.datastore.model.Preference
5 |
6 | val LocalPreference = staticCompositionLocalOf { Preference() }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/authorize/component/ChangePasswordItem.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.authorize.component
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.setValue
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.stringResource
11 | import androidx.compose.ui.text.input.ImeAction
12 | import dev.sanmer.authenticator.R
13 | import dev.sanmer.authenticator.ui.screens.settings.component.SettingBottomSheet
14 |
15 | @Composable
16 | fun ChangePasswordItem(
17 | onDismiss: () -> Unit,
18 | isPasswordError: Boolean,
19 | onChange: (String, String) -> Unit
20 | ) {
21 | var currentPassword by remember { mutableStateOf("") }
22 | var newPassword by remember { mutableStateOf("") }
23 |
24 | SettingBottomSheet(
25 | onDismiss = onDismiss,
26 | title = stringResource(id = R.string.security_change_password),
27 | ) {
28 | PasswordTextField(
29 | password = currentPassword,
30 | onPasswordChange = { currentPassword = it },
31 | imeAction = ImeAction.Next,
32 | isError = isPasswordError,
33 | title = stringResource(id = R.string.security_current_password),
34 | modifier = Modifier.fillMaxWidth()
35 | )
36 |
37 | PasswordTextField(
38 | password = newPassword,
39 | onPasswordChange = { newPassword = it },
40 | onImeDone = { onChange(currentPassword, newPassword) },
41 | title = stringResource(id = R.string.security_new_password),
42 | modifier = Modifier.fillMaxWidth()
43 | )
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/authorize/component/EnterPasswordItem.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.authorize.component
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.setValue
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.stringResource
11 | import dev.sanmer.authenticator.R
12 | import dev.sanmer.authenticator.ui.AuthorizeActivity.Action
13 | import dev.sanmer.authenticator.ui.screens.settings.component.SettingBottomSheet
14 |
15 | @Composable
16 | fun EnterPasswordItem(
17 | onDismiss: () -> Unit,
18 | action: Action,
19 | isPasswordError: Boolean,
20 | onEnter: (String) -> Unit,
21 | enableBiometric: Boolean = false,
22 | onBiometric: () -> Unit = {}
23 | ) {
24 | var currentPassword by remember { mutableStateOf("") }
25 |
26 | SettingBottomSheet(
27 | onDismiss = onDismiss,
28 | title = stringResource(
29 | id = when (action) {
30 | Action.SetupPassword -> R.string.security_set_password
31 | Action.RemovePassword -> R.string.security_remove_password
32 | else -> R.string.security_enter_password
33 | }
34 | ),
35 | ) {
36 | PasswordTextField(
37 | password = currentPassword,
38 | onPasswordChange = { currentPassword = it },
39 | onImeDone = { onEnter(currentPassword) },
40 | isError = isPasswordError,
41 | title = stringResource(
42 | id = when (action) {
43 | Action.SetupPassword -> R.string.security_new_password
44 | else -> R.string.security_current_password
45 | }
46 | ),
47 | modifier = Modifier.fillMaxWidth(),
48 | actionIcon = if (enableBiometric) R.drawable.fingerprint else null,
49 | onActionClick = if (enableBiometric) onBiometric else null
50 | )
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/crypto/CryptoScreen.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.crypto
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.DisposableEffect
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.platform.LocalContext
8 | import androidx.compose.ui.res.stringResource
9 | import androidx.hilt.navigation.compose.hiltViewModel
10 | import dev.sanmer.authenticator.R
11 | import dev.sanmer.authenticator.ktx.finishActivity
12 | import dev.sanmer.authenticator.ui.screens.authorize.component.PasswordTextField
13 | import dev.sanmer.authenticator.ui.screens.settings.component.SettingBottomSheet
14 | import dev.sanmer.authenticator.viewmodel.CryptoViewModel
15 |
16 | @Composable
17 | fun CryptoScreen(
18 | viewModel: CryptoViewModel = hiltViewModel()
19 | ) {
20 | val context = LocalContext.current
21 |
22 | DisposableEffect(viewModel.state) {
23 | if (viewModel.state.isSucceed) context.finishActivity()
24 | onDispose {}
25 | }
26 |
27 | SettingBottomSheet(
28 | onDismiss = context::finishActivity,
29 | title = stringResource(
30 | id = if (viewModel.isEncrypt) {
31 | R.string.crypto_encrypt
32 | } else {
33 | R.string.crypto_decrypt
34 | }
35 | )
36 | ) {
37 | PasswordTextField(
38 | password = viewModel.password,
39 | onPasswordChange = viewModel::updatePassword,
40 | onImeDone = viewModel::crypto,
41 | isError = viewModel.state.isFailed,
42 | title = stringResource(id = R.string.security_password),
43 | modifier = Modifier.fillMaxWidth(),
44 | actionIcon = if (viewModel.isSkip) R.drawable.lock_off else null,
45 | onActionClick = if (viewModel.isSkip) viewModel::crypto else null
46 | )
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/edit/component/DigitsAndPeriodItem.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.edit.component
2 |
3 | import androidx.compose.foundation.layout.Spacer
4 | import androidx.compose.foundation.layout.width
5 | import androidx.compose.foundation.text.KeyboardOptions
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.res.stringResource
9 | import androidx.compose.ui.text.input.ImeAction
10 | import androidx.compose.ui.text.input.KeyboardType
11 | import androidx.compose.ui.unit.dp
12 | import dev.sanmer.authenticator.R
13 | import dev.sanmer.authenticator.model.AuthType
14 |
15 | @Composable
16 | fun DigitsAndPeriodItem(
17 | type: AuthType,
18 | digits: String,
19 | onDigitsChange: (String) -> Unit,
20 | period: String,
21 | onPeriodChange: (String) -> Unit,
22 | readOnly: Boolean
23 | ) = TextFieldContent {
24 | TextField(
25 | value = digits,
26 | onValueChange = onDigitsChange,
27 | label = stringResource(id = R.string.edit_digits),
28 | modifier = Modifier.weight(1f),
29 | keyboardOptions = KeyboardOptions(
30 | keyboardType = KeyboardType.Number,
31 | imeAction = ImeAction.Next
32 | ),
33 | readOnly = readOnly
34 | )
35 |
36 | Spacer(modifier = Modifier.width(15.dp))
37 |
38 | TextField(
39 | value = when (type) {
40 | AuthType.TOTP -> period
41 | },
42 | onValueChange = when (type) {
43 | AuthType.TOTP -> onPeriodChange
44 | },
45 | label = when (type) {
46 | AuthType.TOTP -> stringResource(id = R.string.edit_period)
47 | },
48 | modifier = Modifier.weight(1f),
49 | keyboardOptions = KeyboardOptions(
50 | keyboardType = KeyboardType.Number,
51 | imeAction = ImeAction.Done
52 | ),
53 | readOnly = readOnly
54 | )
55 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/edit/component/TextFieldItem.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.edit.component
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.foundation.text.KeyboardOptions
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.text.input.ImeAction
9 | import androidx.compose.ui.text.input.KeyboardType
10 | import androidx.compose.ui.text.input.PasswordVisualTransformation
11 | import androidx.compose.ui.text.input.VisualTransformation
12 |
13 | @Composable
14 | fun TextFieldItem(
15 | value: String,
16 | onValueChange: (String) -> Unit,
17 | label: String,
18 | modifier: Modifier = Modifier,
19 | @DrawableRes leadingIcon: Int? = null,
20 | trailingIcon: @Composable (() -> Unit)? = null,
21 | hidden: Boolean = false,
22 | isError: Boolean = false,
23 | keyboardOptions: KeyboardOptions = KeyboardOptions(
24 | keyboardType = KeyboardType.Text,
25 | imeAction = ImeAction.Next
26 | )
27 | ) = TextFieldContent(
28 | modifier = modifier,
29 | leading = leadingIcon?.let { { TextFieldContentIcon(icon = it) } },
30 | trailing = trailingIcon
31 | ) {
32 | val passwordVisualTransformation = remember { PasswordVisualTransformation() }
33 |
34 | TextField(
35 | value = value,
36 | onValueChange = onValueChange,
37 | label = label,
38 | modifier = Modifier.weight(1f),
39 | readOnly = hidden,
40 | isError = isError,
41 | keyboardOptions = keyboardOptions,
42 | visualTransformation = if (hidden) {
43 | passwordVisualTransformation
44 | } else {
45 | VisualTransformation.None
46 | },
47 | )
48 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/edit/component/TypeAndHashItem.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.edit.component
2 |
3 | import androidx.compose.foundation.layout.Spacer
4 | import androidx.compose.foundation.layout.width
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.res.stringResource
8 | import androidx.compose.ui.unit.dp
9 | import dev.sanmer.authenticator.R
10 | import dev.sanmer.authenticator.model.AuthType
11 | import dev.sanmer.otp.HOTP
12 |
13 | @Composable
14 | fun TypeAndHashItem(
15 | type: AuthType,
16 | onTypeChange: (AuthType) -> Unit,
17 | hash: HOTP.Hash,
18 | onHashChange: (HOTP.Hash) -> Unit,
19 | readOnly: Boolean
20 | ) = TextFieldContent(
21 | leading = { TextFieldContentIcon(icon = R.drawable.math_function) }
22 | ) {
23 | TextFieldDropdownMenu(
24 | value = type,
25 | values = AuthType.entries,
26 | onValueChange = onTypeChange,
27 | label = stringResource(id = R.string.edit_type),
28 | readOnly = readOnly,
29 | modifier = Modifier.weight(1f)
30 | )
31 |
32 | Spacer(modifier = Modifier.width(15.dp))
33 |
34 | TextFieldDropdownMenu(
35 | value = hash,
36 | values = HOTP.Hash.entries,
37 | onValueChange = onHashChange,
38 | label = stringResource(id = R.string.edit_hash),
39 | readOnly = readOnly,
40 | modifier = Modifier.weight(1f)
41 | )
42 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/home/component/AuthList.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.home.component
2 |
3 | import androidx.compose.animation.animateContentSize
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.lazy.LazyListState
9 | import androidx.compose.foundation.lazy.items
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.unit.dp
14 | import androidx.navigation.NavController
15 | import dev.sanmer.authenticator.database.entity.TotpEntity
16 | import dev.sanmer.authenticator.model.impl.TotpImpl
17 | import dev.sanmer.authenticator.ui.ktx.navigateSingleTopTo
18 | import dev.sanmer.authenticator.ui.ktx.plus
19 | import dev.sanmer.authenticator.ui.main.Screen
20 |
21 | @Composable
22 | fun AuthList(
23 | state: LazyListState,
24 | navController: NavController,
25 | totp: List,
26 | recycle: (TotpEntity) -> Unit,
27 | contentPadding: PaddingValues = PaddingValues(0.dp)
28 | ) {
29 | LazyColumn(
30 | modifier = Modifier
31 | .fillMaxWidth()
32 | .animateContentSize(),
33 | state = state,
34 | contentPadding = contentPadding + PaddingValues(15.dp),
35 | verticalArrangement = Arrangement.spacedBy(15.dp),
36 | horizontalAlignment = Alignment.CenterHorizontally
37 | ) {
38 | items(totp) {
39 | AuthItem(
40 | auth = it,
41 | enabled = false,
42 | onEdit = { navController.navigateSingleTopTo(Screen.Edit(it.entity.id)) },
43 | onDelete = { recycle(it.entity) }
44 | )
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/ntp/component/NtpList.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.ntp.component
2 |
3 | import androidx.compose.animation.animateContentSize
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.lazy.LazyListState
9 | import androidx.compose.foundation.lazy.items
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.unit.dp
14 | import dev.sanmer.authenticator.datastore.model.Ntp
15 | import dev.sanmer.authenticator.ui.ktx.plus
16 | import dev.sanmer.authenticator.ui.provider.LocalPreference
17 | import dev.sanmer.authenticator.viewmodel.NtpViewModel
18 |
19 | @Composable
20 | fun NtpList(
21 | state: LazyListState,
22 | ntps: List,
23 | setNtp: (Ntp) -> Unit,
24 | contentPadding: PaddingValues = PaddingValues(0.dp)
25 | ) {
26 | val preference = LocalPreference.current
27 |
28 | LazyColumn(
29 | modifier = Modifier
30 | .fillMaxWidth()
31 | .animateContentSize(),
32 | state = state,
33 | contentPadding = contentPadding + PaddingValues(15.dp),
34 | verticalArrangement = Arrangement.spacedBy(15.dp),
35 | horizontalAlignment = Alignment.CenterHorizontally
36 | ) {
37 | items(ntps) {
38 | NtpItem(
39 | ntp = it,
40 | selected = preference.ntp == it.ntp,
41 | onClick = { setNtp(it.ntp) }
42 | )
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/settings/component/PreferenceItem.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.settings.component
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.res.stringResource
5 | import androidx.navigation.NavController
6 | import dev.sanmer.authenticator.R
7 | import dev.sanmer.authenticator.ui.ktx.navigateSingleTopTo
8 | import dev.sanmer.authenticator.ui.main.Screen
9 |
10 | @Composable
11 | fun PreferenceItem(
12 | onDismiss: () -> Unit,
13 | navController: NavController
14 | ) {
15 | SettingBottomSheet(
16 | onDismiss = onDismiss,
17 | title = stringResource(id = R.string.settings_preference)
18 | ) {
19 | SettingItem(
20 | icon = R.drawable.shield,
21 | title = stringResource(id = R.string.settings_security),
22 | onClick = {
23 | navController.navigateSingleTopTo(Screen.Security())
24 | onDismiss()
25 | }
26 | )
27 |
28 | SettingItem(
29 | icon = R.drawable.timezone,
30 | title = stringResource(id = R.string.settings_ntp_server),
31 | onClick = {
32 | navController.navigateSingleTopTo(Screen.Ntp())
33 | onDismiss()
34 | }
35 | )
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/settings/component/TokenItem.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.settings.component
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.res.stringResource
5 | import androidx.navigation.NavController
6 | import dev.sanmer.authenticator.R
7 | import dev.sanmer.authenticator.ui.ktx.navigateSingleTopTo
8 | import dev.sanmer.authenticator.ui.main.Screen
9 |
10 | @Composable
11 | fun TokenItem(
12 | onDismiss: () -> Unit,
13 | navController: NavController,
14 | ) {
15 | SettingBottomSheet(
16 | onDismiss = onDismiss,
17 | title = stringResource(id = R.string.settings_token)
18 | ) {
19 | SettingItem(
20 | icon = R.drawable.pencil_plus,
21 | title = stringResource(id = R.string.settings_enter),
22 | onClick = {
23 | navController.navigateSingleTopTo(Screen.Edit())
24 | onDismiss()
25 | }
26 | )
27 |
28 | SettingItem(
29 | icon = R.drawable.scan,
30 | title = stringResource(id = R.string.settings_scan),
31 | onClick = {
32 | navController.navigateSingleTopTo(Screen.Scan())
33 | onDismiss()
34 | }
35 | )
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/settings/component/ToolItem.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.settings.component
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import androidx.activity.compose.rememberLauncherForActivityResult
6 | import androidx.activity.result.contract.ActivityResultContracts
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.platform.LocalContext
9 | import androidx.compose.ui.res.stringResource
10 | import androidx.navigation.NavController
11 | import dev.sanmer.authenticator.R
12 | import dev.sanmer.authenticator.model.serializer.AuthJson
13 | import dev.sanmer.authenticator.ui.ktx.navigateSingleTopTo
14 | import dev.sanmer.authenticator.ui.main.Screen
15 |
16 | @Composable
17 | fun ToolItem(
18 | onDismiss: () -> Unit,
19 | navController: NavController,
20 | decryptFromJson: (Context, Uri, () -> Unit) -> Unit,
21 | decryptedToJson: (Context, Uri) -> Unit
22 | ) {
23 | val context = LocalContext.current
24 | val jsonDecrypt = rememberLauncherForActivityResult(
25 | contract = ActivityResultContracts.CreateDocument(AuthJson.MIME_TYPE),
26 | onResult = { if (it != null) decryptedToJson(context, it) }
27 | )
28 | val jsonEncrypt = rememberLauncherForActivityResult(
29 | contract = ActivityResultContracts.GetContent(),
30 | onResult = {
31 | if (it != null) decryptFromJson(context, it) {
32 | jsonDecrypt.launch(AuthJson.FILE_NAME)
33 | }
34 | }
35 | )
36 |
37 | SettingBottomSheet(
38 | onDismiss = onDismiss,
39 | title = stringResource(id = R.string.settings_tools),
40 | ) {
41 | SettingItem(
42 | icon = R.drawable.lock_open,
43 | title = stringResource(id = R.string.settings_decrypt),
44 | onClick = { jsonEncrypt.launch(AuthJson.MIME_TYPE) }
45 | )
46 |
47 | SettingItem(
48 | icon = R.drawable.a_b,
49 | title = stringResource(id = R.string.settings_encode_decode),
50 | onClick = {
51 | navController.navigateSingleTopTo(Screen.Encode())
52 | onDismiss()
53 | }
54 | )
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/screens/trash/component/AuthList.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.screens.trash.component
2 |
3 | import androidx.compose.animation.animateContentSize
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.lazy.LazyListState
9 | import androidx.compose.foundation.lazy.items
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.unit.dp
14 | import dev.sanmer.authenticator.database.entity.TotpEntity
15 | import dev.sanmer.authenticator.ui.ktx.plus
16 |
17 | @Composable
18 | fun AuthList(
19 | state: LazyListState,
20 | totp: List,
21 | restore: (TotpEntity) -> Unit,
22 | delete: (TotpEntity) -> Unit,
23 | contentPadding: PaddingValues = PaddingValues(0.dp)
24 | ) = LazyColumn(
25 | modifier = Modifier
26 | .fillMaxWidth()
27 | .animateContentSize(),
28 | state = state,
29 | contentPadding = contentPadding + PaddingValues(15.dp),
30 | verticalArrangement = Arrangement.spacedBy(15.dp),
31 | horizontalAlignment = Alignment.CenterHorizontally
32 | ) {
33 | items(totp) {
34 | AuthItem(
35 | entity = it,
36 | onRestore = { restore(it) },
37 | onDelete = { delete(it) }
38 | )
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.theme
2 |
3 | import androidx.compose.material3.Shapes
4 |
5 | val Shapes = Shapes()
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | val Typography = Typography(
10 | titleLarge = TextStyle(
11 | fontFamily = FontFamily.SansSerif,
12 | fontWeight = FontWeight.Medium,
13 | fontSize = 22.sp,
14 | lineHeight = 28.sp,
15 | letterSpacing = 0.sp,
16 | )
17 | )
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/viewmodel/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.viewmodel
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import dev.sanmer.authenticator.datastore.model.Preference
10 | import dev.sanmer.authenticator.repository.PreferenceRepository
11 | import kotlinx.coroutines.launch
12 | import timber.log.Timber
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | class MainViewModel @Inject constructor(
17 | private val preferenceRepository: PreferenceRepository
18 | ) : ViewModel() {
19 | var loadState by mutableStateOf(LoadState.Pending)
20 | private set
21 |
22 | val isPending inline get() = loadState.isPending
23 | val isLocked inline get() = loadState.isLocked
24 | val preference inline get() = loadState.preference
25 |
26 | init {
27 | Timber.d("MainViewModel init")
28 | preferenceObserver()
29 | }
30 |
31 | private fun preferenceObserver() {
32 | viewModelScope.launch {
33 | preferenceRepository.data.collect {
34 | loadState = if (loadState.isReady || !it.isEncrypted ) {
35 | LoadState.Ready(it)
36 | } else {
37 | LoadState.Locked(it)
38 | }
39 | }
40 | }
41 | }
42 |
43 | fun setUnlocked() {
44 | loadState = LoadState.Ready(preference)
45 | }
46 |
47 | sealed class LoadState {
48 | abstract val preference: Preference
49 |
50 | data object Pending : LoadState() {
51 | override val preference = Preference()
52 | }
53 |
54 | data class Locked(
55 | override val preference: Preference
56 | ) : LoadState()
57 |
58 | data class Ready(
59 | override val preference: Preference
60 | ) : LoadState()
61 |
62 | val isPending inline get() = this is Pending
63 | val isLocked inline get() = this is Locked
64 | val isReady inline get() = this is Ready
65 | }
66 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/authenticator/viewmodel/TrashViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.authenticator.viewmodel
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import dev.sanmer.authenticator.database.entity.TotpEntity
10 | import dev.sanmer.authenticator.repository.DbRepository
11 | import kotlinx.coroutines.launch
12 | import timber.log.Timber
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | class TrashViewModel @Inject constructor(
17 | private val dbRepository: DbRepository
18 | ) : ViewModel() {
19 | var loadState by mutableStateOf(LoadState.Pending)
20 | private set
21 | val totp inline get() = loadState.totp
22 | val isPending inline get() = loadState.isPending
23 |
24 | init {
25 | Timber.d("TrashViewModel init")
26 | dataObserver()
27 | }
28 |
29 | private fun dataObserver() {
30 | viewModelScope.launch {
31 | dbRepository.getTotpAllDecryptedTrashedAsFlow()
32 | .collect { totp ->
33 | loadState = LoadState.Ready(totp)
34 | }
35 | }
36 | }
37 |
38 | fun restoreAll() {
39 | viewModelScope.launch {
40 | dbRepository.updateTotp(totp.map { it.copy(deletedAt = 0) })
41 | }
42 | }
43 |
44 | fun restore(entity: TotpEntity) {
45 | viewModelScope.launch {
46 | dbRepository.updateTotp(entity.copy(deletedAt = 0))
47 | }
48 | }
49 |
50 | fun delete(entity: TotpEntity) {
51 | viewModelScope.launch {
52 | dbRepository.deleteTotp(entity)
53 | }
54 | }
55 |
56 | sealed class LoadState {
57 | abstract val totp: List
58 |
59 | data object Pending : LoadState() {
60 | override val totp = emptyList()
61 | }
62 |
63 | data class Ready(
64 | override val totp: List
65 | ) : LoadState()
66 |
67 | val isPending inline get() = this is Pending
68 | }
69 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/a_b.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/alert_triangle.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_bar_down.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_bar_up.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_left.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/brand_github_2.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/camera_off.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/caret_down.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/caret_up.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/check.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/circle_check_filled.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/clipboard_text.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/database.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/database_export.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/database_import.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/device_floppy.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/edit.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/eye.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/eye_closed.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/file_export.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/file_import.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/fingerprint.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/key.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
17 |
23 |
29 |
35 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/launcher_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/lock.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/lock_off.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/lock_open.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/math_function.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/mood_heart.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/pencil_plus.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/photo.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/refresh.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/restore.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/scan.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/search.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings_2.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shield.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shield_off.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/timezone.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/tool.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/trash.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/trash_x.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/user.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/x.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night-v31/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @android:color/system_accent1_200
4 | @android:color/system_accent1_800
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FBFCFD
4 | #0A0C10
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v31/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @android:color/system_accent1_600
4 | @android:color/system_accent1_0
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #0A0C10
4 | #FBFCFD
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings_untranslatable.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Authenticator
4 | ••••••••
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
17 |
18 |
24 |
25 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/locales_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/build-logic/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | dependencies {
6 | compileOnly(libs.android.gradle)
7 | compileOnly(libs.compose.gradle)
8 | compileOnly(libs.kotlin.gradle)
9 | compileOnly(libs.ksp.gradle)
10 | }
11 |
12 | gradlePlugin {
13 | plugins {
14 | register("self.application") {
15 | id = "self.application"
16 | implementationClass = "ApplicationConventionPlugin"
17 | }
18 |
19 | register("self.library") {
20 | id = "self.library"
21 | implementationClass = "LibraryConventionPlugin"
22 | }
23 |
24 | register("self.compose") {
25 | id = "self.compose"
26 | implementationClass = "ComposeConventionPlugin"
27 | }
28 |
29 | register("self.hilt") {
30 | id = "self.hilt"
31 | implementationClass = "HiltConventionPlugin"
32 | }
33 |
34 | register("self.room") {
35 | id = "self.room"
36 | implementationClass = "RoomConventionPlugin"
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/build-logic/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 |
7 | versionCatalogs {
8 | create("libs") {
9 | from(files("../gradle/libs.versions.toml"))
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/ApplicationConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.ApplicationExtension
2 | import org.gradle.api.JavaVersion
3 | import org.gradle.api.Plugin
4 | import org.gradle.api.Project
5 | import org.gradle.api.plugins.JavaPluginExtension
6 | import org.gradle.jvm.toolchain.JavaLanguageVersion
7 | import org.gradle.kotlin.dsl.apply
8 | import org.gradle.kotlin.dsl.configure
9 | import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
10 |
11 | class ApplicationConventionPlugin : Plugin {
12 | override fun apply(target: Project) = with(target) {
13 | apply(plugin = "com.android.application")
14 | apply(plugin = "org.jetbrains.kotlin.android")
15 |
16 | extensions.configure {
17 | compileSdk = 35
18 | buildToolsVersion = "35.0.1"
19 |
20 | defaultConfig {
21 | minSdk = 33
22 | targetSdk = compileSdk
23 | }
24 |
25 | compileOptions {
26 | sourceCompatibility = JavaVersion.VERSION_21
27 | targetCompatibility = JavaVersion.VERSION_21
28 | }
29 | }
30 |
31 | extensions.configure {
32 | toolchain {
33 | languageVersion.set(JavaLanguageVersion.of(21))
34 | }
35 | }
36 |
37 | extensions.configure {
38 | jvmToolchain(21)
39 |
40 | sourceSets.all {
41 | languageSettings {
42 | optIn("kotlinx.serialization.ExperimentalSerializationApi")
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/ComposeConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.ApplicationExtension
2 | import org.gradle.api.Plugin
3 | import org.gradle.api.Project
4 | import org.gradle.api.artifacts.VersionCatalogsExtension
5 | import org.gradle.kotlin.dsl.apply
6 | import org.gradle.kotlin.dsl.configure
7 | import org.gradle.kotlin.dsl.dependencies
8 | import org.gradle.kotlin.dsl.getByType
9 | import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
10 |
11 | class ComposeConventionPlugin : Plugin {
12 | override fun apply(target: Project) = with(target) {
13 | apply(plugin = "com.android.application")
14 | apply(plugin = "org.jetbrains.kotlin.android")
15 | apply(plugin = "org.jetbrains.kotlin.plugin.compose")
16 |
17 | extensions.configure {
18 | buildFeatures {
19 | compose = true
20 | }
21 | }
22 |
23 | extensions.configure {
24 | sourceSets.all {
25 | languageSettings {
26 | optIn("androidx.compose.material3.ExperimentalMaterial3Api")
27 | }
28 | }
29 | }
30 |
31 | val libs = extensions.getByType().named("libs")
32 | dependencies {
33 | "implementation"(libs.findLibrary("androidx.compose.material3").get())
34 | "implementation"(libs.findLibrary("androidx.compose.ui").get())
35 | "implementation"(libs.findLibrary("androidx.compose.ui.tooling.preview").get())
36 | "debugImplementation"(libs.findLibrary("androidx.compose.ui.tooling").get())
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/HiltConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.api.artifacts.VersionCatalogsExtension
4 | import org.gradle.kotlin.dsl.apply
5 | import org.gradle.kotlin.dsl.dependencies
6 | import org.gradle.kotlin.dsl.getByType
7 |
8 | class HiltConventionPlugin : Plugin {
9 | override fun apply(target: Project) = with(target) {
10 | apply(plugin = "dagger.hilt.android.plugin")
11 | apply(plugin = "com.google.devtools.ksp")
12 |
13 | val libs = extensions.getByType().named("libs")
14 | dependencies {
15 | "implementation"(libs.findLibrary("hilt.android").get())
16 | "ksp"(libs.findLibrary("hilt.compiler").get())
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/LibraryConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.LibraryExtension
2 | import org.gradle.api.JavaVersion
3 | import org.gradle.api.Plugin
4 | import org.gradle.api.Project
5 | import org.gradle.api.plugins.JavaPluginExtension
6 | import org.gradle.jvm.toolchain.JavaLanguageVersion
7 | import org.gradle.kotlin.dsl.apply
8 | import org.gradle.kotlin.dsl.configure
9 | import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
10 |
11 | class LibraryConventionPlugin : Plugin {
12 | override fun apply(target: Project) = with(target) {
13 | apply(plugin = "com.android.library")
14 | apply(plugin = "org.jetbrains.kotlin.android")
15 |
16 | extensions.configure {
17 | compileSdk = 35
18 | buildToolsVersion = "35.0.1"
19 |
20 | defaultConfig {
21 | minSdk = 31
22 | }
23 |
24 | compileOptions {
25 | sourceCompatibility = JavaVersion.VERSION_21
26 | targetCompatibility = JavaVersion.VERSION_21
27 | }
28 | }
29 |
30 | extensions.configure {
31 | toolchain {
32 | languageVersion.set(JavaLanguageVersion.of(21))
33 | }
34 | }
35 |
36 | extensions.configure {
37 | jvmToolchain(21)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/ProjectExt.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Project
2 | import org.gradle.kotlin.dsl.extra
3 | import java.io.File
4 | import java.util.Properties
5 |
6 | val Project.commitSha: String get() = exec("git rev-parse HEAD")
7 | val Project.commitCount: Int get() = exec("git rev-list --count HEAD").toInt()
8 |
9 | @Suppress("UnstableApiUsage")
10 | fun Project.exec(command: String) = providers.exec {
11 | commandLine(command.split(" "))
12 | }.standardOutput.asText.get().trim()
13 |
14 | val Project.releaseKeyStore: File get() = File(extra["keyStore"] as String)
15 | val Project.releaseKeyStorePassword: String get() = extra["keyStorePassword"] as String
16 | val Project.releaseKeyAlias: String get() = extra["keyAlias"] as String
17 | val Project.releaseKeyPassword: String get() = extra["keyPassword"] as String
18 | val Project.hasReleaseKeyStore: Boolean get() {
19 | signingProperties(rootDir).forEach { key, value ->
20 | extra[key as String] = value
21 | }
22 |
23 | return extra.has("keyStore")
24 | }
25 |
26 | private fun signingProperties(rootDir: File): Properties {
27 | val properties = Properties()
28 | val signingProperties = rootDir.resolve("signing.properties")
29 | if (signingProperties.isFile) {
30 | signingProperties.inputStream().use(properties::load)
31 | }
32 |
33 | return properties
34 | }
--------------------------------------------------------------------------------
/build-logic/src/main/kotlin/RoomConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.google.devtools.ksp.gradle.KspExtension
2 | import org.gradle.api.Plugin
3 | import org.gradle.api.Project
4 | import org.gradle.api.artifacts.VersionCatalogsExtension
5 | import org.gradle.kotlin.dsl.apply
6 | import org.gradle.kotlin.dsl.configure
7 | import org.gradle.kotlin.dsl.dependencies
8 | import org.gradle.kotlin.dsl.getByType
9 |
10 | class RoomConventionPlugin : Plugin {
11 | override fun apply(target: Project) = with(target) {
12 | apply(plugin = "com.google.devtools.ksp")
13 |
14 | extensions.configure {
15 | arg("room.incremental", "true")
16 | arg("room.expandProjection", "true")
17 | arg("room.schemaLocation", "$projectDir/schemas")
18 | }
19 |
20 | val libs = extensions.getByType().named("libs")
21 | dependencies {
22 | "implementation"(libs.findLibrary("androidx.room.ktx").get())
23 | "implementation"(libs.findLibrary("androidx.room.runtime").get())
24 | "ksp"(libs.findLibrary("androidx.room.compiler").get())
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application) apply false
3 | alias(libs.plugins.android.library) apply false
4 | alias(libs.plugins.compose.compiler) apply false
5 | alias(libs.plugins.kotlin.jvm) apply false
6 | alias(libs.plugins.kotlin.serialization) apply false
7 | alias(libs.plugins.hilt) apply false
8 | alias(libs.plugins.ksp) apply false
9 | }
10 |
11 | tasks.register("clean") {
12 | delete(layout.buildDirectory)
13 | }
14 |
--------------------------------------------------------------------------------
/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.self.library)
3 | }
4 |
5 | android {
6 | namespace = "dev.sanmer.core"
7 | }
8 |
9 | dependencies {
10 | implementation(libs.androidx.annotation)
11 | implementation(libs.encoding.base32)
12 | implementation(libs.encoding.base64)
13 | implementation(libs.google.zxing)
14 | implementation(libs.kotlinx.coroutines.android)
15 | }
16 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/crypto/Crypto.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.crypto
2 |
3 | import dev.sanmer.encoding.decodeBase64
4 | import dev.sanmer.encoding.encodeBase64
5 | import java.security.SecureRandom
6 | import javax.crypto.SecretKey
7 | import javax.crypto.SecretKeyFactory
8 | import javax.crypto.spec.PBEKeySpec
9 | import javax.crypto.spec.SecretKeySpec
10 |
11 | interface Crypto {
12 | suspend fun encrypt(input: ByteArray): ByteArray
13 | suspend fun encrypt(input: String) = encrypt(input.toByteArray(Charsets.UTF_8)).encodeBase64()
14 |
15 | suspend fun decrypt(input: ByteArray): ByteArray
16 | suspend fun decrypt(input: String) = decrypt(input.decodeBase64()).toString(Charsets.UTF_8)
17 |
18 | companion object Default {
19 | const val FACTORY_ALGORITHM = "PBKDF2WithHmacSHA256"
20 | const val ALGORITHM = "AES/GCM/NoPadding"
21 | const val KEY_ALGORITHM = "AES"
22 | const val ITERATION_COUNT = 102400
23 | const val KEY_LENGTH = 256
24 | const val SALT_LENGTH = 16
25 | const val IV_LENGTH = 12
26 | const val TAG_LENGTH = 128
27 |
28 | val randomSalt: ByteArray
29 | inline get() = ByteArray(SALT_LENGTH).apply {
30 | SecureRandom().nextBytes(this)
31 | }
32 |
33 | val randomIv: ByteArray
34 | inline get() = ByteArray(IV_LENGTH).apply {
35 | SecureRandom().nextBytes(this)
36 | }
37 |
38 | fun CharArray.generateSecretKey(salt: ByteArray): SecretKey {
39 | val factory = SecretKeyFactory.getInstance(FACTORY_ALGORITHM)
40 | val spec = PBEKeySpec(this, salt, ITERATION_COUNT, KEY_LENGTH)
41 | val secret = factory.generateSecret(spec)
42 | return SecretKeySpec(secret.encoded, KEY_ALGORITHM)
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/crypto/PasswordKey.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.crypto
2 |
3 | import dev.sanmer.crypto.Crypto.Default.generateSecretKey
4 | import dev.sanmer.crypto.Crypto.Default.randomIv
5 | import dev.sanmer.crypto.Crypto.Default.randomSalt
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.withContext
8 | import javax.crypto.Cipher
9 | import javax.crypto.SecretKey
10 | import javax.crypto.spec.GCMParameterSpec
11 |
12 | class PasswordKey private constructor(
13 | private val password: CharArray
14 | ) : Crypto {
15 | private val salt by lazy { randomSalt }
16 |
17 | private val keys = hashMapOf()
18 | private fun generateSecretKey(salt: ByteArray): SecretKey {
19 | return keys.getOrPut(salt.contentHashCode()) {
20 | password.generateSecretKey(salt)
21 | }
22 | }
23 |
24 | override suspend fun encrypt(input: ByteArray) = withContext(Dispatchers.IO) {
25 | val iv = randomIv
26 | val key = generateSecretKey(salt)
27 | val cipher = Cipher.getInstance(Crypto.ALGORITHM)
28 | cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(Crypto.TAG_LENGTH, iv))
29 | salt + iv + cipher.doFinal(input)
30 | }
31 |
32 | override suspend fun decrypt(input: ByteArray) = withContext(Dispatchers.IO) {
33 | val salt = input.copyOfRange(0, Crypto.SALT_LENGTH)
34 | val iv = input.copyOfRange(Crypto.SALT_LENGTH, Crypto.SALT_LENGTH + Crypto.IV_LENGTH)
35 | val data = input.copyOfRange(Crypto.SALT_LENGTH + Crypto.IV_LENGTH, input.size)
36 |
37 | val key = generateSecretKey(salt)
38 | val cipher = Cipher.getInstance(Crypto.ALGORITHM)
39 | cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(Crypto.TAG_LENGTH, iv))
40 | cipher.doFinal(data)
41 | }
42 |
43 | companion object Default {
44 | fun new(password: String) = PasswordKey(password.toCharArray())
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/crypto/SessionKey.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.crypto
2 |
3 | import dev.sanmer.crypto.Crypto.Default.randomIv
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.withContext
6 | import javax.crypto.Cipher
7 | import javax.crypto.KeyGenerator
8 | import javax.crypto.SecretKey
9 | import javax.crypto.spec.GCMParameterSpec
10 | import javax.crypto.spec.SecretKeySpec
11 |
12 | class SessionKey(
13 | val key: SecretKey
14 | ) : Crypto {
15 | override suspend fun encrypt(input: ByteArray) = withContext(Dispatchers.IO) {
16 | val iv = randomIv
17 | val cipher = Cipher.getInstance(Crypto.ALGORITHM)
18 | cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(Crypto.TAG_LENGTH, iv))
19 | iv + cipher.doFinal(input)
20 | }
21 |
22 | override suspend fun decrypt(input: ByteArray) = withContext(Dispatchers.IO) {
23 | val iv = input.copyOfRange(0, Crypto.IV_LENGTH)
24 | val data = input.copyOfRange(Crypto.IV_LENGTH, input.size)
25 |
26 | val cipher = Cipher.getInstance(Crypto.ALGORITHM)
27 | cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(Crypto.TAG_LENGTH, iv))
28 | cipher.doFinal(data)
29 | }
30 |
31 | suspend fun getKeyEncryptedByPassword(password: String) =
32 | PasswordKey.new(password).encrypt(key.encoded)
33 |
34 | companion object Default {
35 | fun ByteArray.toSecretKey() =
36 | SecretKeySpec(this, 0, size, Crypto.KEY_ALGORITHM)
37 |
38 | fun new() = SessionKey(
39 | key = KeyGenerator.getInstance(Crypto.KEY_ALGORITHM).apply {
40 | init(Crypto.KEY_LENGTH)
41 | }.generateKey()
42 | )
43 |
44 | suspend fun decryptKeyByPassword(key: ByteArray, password: String) = SessionKey(
45 | key = PasswordKey.new(password).decrypt(key).toSecretKey()
46 | )
47 | }
48 | }
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/encoding/Base32.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE")
2 |
3 | package dev.sanmer.encoding
4 |
5 | import io.matthewnelson.encoding.base32.Base32
6 | import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull
7 | import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
8 | import io.matthewnelson.encoding.core.EncodingException
9 | import java.nio.charset.Charset
10 |
11 | inline fun String.isBase32(): Boolean {
12 | return isNotBlank() && (decodeToByteArrayOrNull(Base32.Default) != null
13 | || decodeToByteArrayOrNull(Base32.Hex) != null)
14 | }
15 |
16 | inline fun String.decodeBase32(): ByteArray {
17 | return decodeToByteArrayOrNull(Base32.Default)
18 | ?: decodeToByteArrayOrNull(Base32.Hex)
19 | ?: throw EncodingException(this)
20 | }
21 |
22 | inline fun String.decodeBase32(charset: Charset = Charsets.UTF_8) =
23 | decodeBase32().toString(charset)
24 |
25 | inline fun ByteArray.encodeBase32Default() = encodeToString(Base32.Default)
26 |
27 | inline fun ByteArray.encodeBase32Hex() = encodeToString(Base32.Hex)
28 |
29 | inline fun String.encodeBase32Default(charset: Charset = Charsets.UTF_8) =
30 | toByteArray(charset).encodeBase32Default()
31 |
32 | inline fun String.encodeBase32Hex(charset: Charset = Charsets.UTF_8) =
33 | toByteArray(charset).encodeBase32Hex()
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/encoding/Base64.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE")
2 |
3 | package dev.sanmer.encoding
4 |
5 | import io.matthewnelson.encoding.base64.Base64
6 | import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
7 | import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull
8 | import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
9 | import java.nio.charset.Charset
10 |
11 | inline fun String.isBase64() = isNotBlank() && decodeToByteArrayOrNull(Base64.Default) != null
12 |
13 | inline fun String.decodeBase64() = decodeToByteArray(Base64.Default)
14 |
15 | inline fun String.decodeBase64(charset: Charset = Charsets.UTF_8) =
16 | decodeToByteArray(Base64.Default).toString(charset)
17 |
18 | inline fun ByteArray.encodeBase64() = encodeToString(Base64.Default)
19 |
20 | inline fun String.encodeBase64(charset: Charset = Charsets.UTF_8) =
21 | toByteArray(charset).encodeBase64()
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/otp/ByteArray.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE")
2 |
3 | package dev.sanmer.otp
4 |
5 | import java.nio.ByteBuffer
6 | import java.nio.ByteOrder
7 | import javax.crypto.Mac
8 | import javax.crypto.spec.SecretKeySpec
9 | import kotlin.math.pow
10 |
11 | internal inline fun Long.toByteArray() =
12 | ByteBuffer.allocate(Long.SIZE_BYTES)
13 | .order(ByteOrder.BIG_ENDIAN)
14 | .putLong(this)
15 | .array()
16 |
17 | internal inline fun ByteArray.hmac(key: ByteArray, algorithm: String): ByteArray =
18 | Mac.getInstance(algorithm).let {
19 | it.init(SecretKeySpec(key, algorithm))
20 | it.doFinal(this)
21 | }
22 |
23 | internal inline fun ByteArray.hmacSha1(key: ByteArray) = hmac(key, "HmacSHA1")
24 |
25 | internal inline fun ByteArray.hmacSha256(key: ByteArray) = hmac(key, "HmacSHA256")
26 |
27 | internal inline fun ByteArray.hmacSha512(key: ByteArray) = hmac(key, "HmacSHA512")
28 |
29 | internal inline fun ByteArray.otp(digits: Int): Int {
30 | val offset = this[size - 1].toInt() and 0x0F
31 | val code = ((this[offset].toInt() and 0x7F shl 24)
32 | or (this[offset + 1].toInt() and 0xFF shl 16)
33 | or (this[offset + 2].toInt() and 0xFF shl 8)
34 | or (this[offset + 3].toInt() and 0xFF))
35 |
36 | return code % 10.0.pow(digits).toInt()
37 | }
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/otp/HOTP.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.otp
2 |
3 | import dev.sanmer.encoding.decodeBase32
4 |
5 | object HOTP {
6 | enum class Hash {
7 | SHA1,
8 | SHA256,
9 | SHA512
10 | }
11 |
12 | private fun otp(
13 | hash: Hash,
14 | secret: ByteArray,
15 | counter: ByteArray,
16 | digits: Int
17 | ) = when (hash) {
18 | Hash.SHA1 -> counter.hmacSha1(secret)
19 | Hash.SHA256 -> counter.hmacSha256(secret)
20 | Hash.SHA512 -> counter.hmacSha512(secret)
21 | }.otp(digits)
22 |
23 | fun otp(
24 | hash: Hash,
25 | secret: ByteArray,
26 | counter: Long,
27 | digits: Int
28 | ) = otp(
29 | hash = hash,
30 | secret = secret,
31 | counter = counter.toByteArray(),
32 | digits = digits
33 | ).toString().padStart(digits, '0')
34 |
35 | fun otp(
36 | hash: Hash,
37 | secret: String,
38 | counter: Long,
39 | digits: Int
40 | ) = otp(
41 | hash = hash,
42 | secret = secret.decodeBase32(),
43 | counter = counter.toByteArray(),
44 | digits = digits
45 | ).toString().padStart(digits, '0')
46 | }
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/otp/TOTP.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.otp
2 |
3 | object TOTP {
4 | val epochSeconds: Long
5 | inline get() = System.currentTimeMillis() / 1000
6 |
7 | fun otp(
8 | hash: HOTP.Hash,
9 | secret: ByteArray,
10 | digits: Int,
11 | period: Long,
12 | seconds: Long = epochSeconds
13 | ) = HOTP.otp(
14 | hash = hash,
15 | secret = secret,
16 | counter = seconds / period,
17 | digits = digits
18 | )
19 |
20 | fun otp(
21 | hash: HOTP.Hash,
22 | secret: String,
23 | digits: Int,
24 | period: Long,
25 | seconds: Long = epochSeconds
26 | ) = HOTP.otp(
27 | hash = hash,
28 | secret = secret,
29 | counter = seconds / period,
30 | digits = digits
31 | )
32 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | org.gradle.caching=true
3 | org.gradle.configuration-cache=true
4 |
5 | android.useAndroidX=true
6 | android.nonTransitiveRClass=true
7 |
8 | kapt.include.compile.classpath=false
9 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/logo/afphoto/aliyun.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/aliyun.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/apple.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/apple.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/aws.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/aws.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/azure.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/azure.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/binance.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/binance.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/bybit.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/bybit.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/cloudflare.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/cloudflare.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/coinbase.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/coinbase.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/crowdin.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/crowdin.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/digitalocean.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/digitalocean.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/discord.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/discord.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/facebook.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/facebook.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/gitea.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/gitea.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/github.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/github.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/gitlab.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/gitlab.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/google.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/google.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/instagram.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/instagram.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/jetbrains.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/jetbrains.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/lark.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/lark.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/mega.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/mega.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/meta.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/meta.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/microsoft.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/microsoft.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/okx.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/okx.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/onlyfans.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/onlyfans.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/openai.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/openai.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/orcid.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/orcid.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/patreon.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/patreon.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/paypal.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/paypal.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/pixiv.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/pixiv.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/tencent-cloud.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/tencent-cloud.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/wise.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/wise.afphoto
--------------------------------------------------------------------------------
/logo/afphoto/x.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SanmerApps/Authenticator/a90e9f5d15a3ed75399f6ad841260adb6f103d5f/logo/afphoto/x.afphoto
--------------------------------------------------------------------------------
/logo/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.self.library)
3 | }
4 |
5 | android {
6 | namespace = "dev.sanmer.logo"
7 | }
8 |
9 | dependencies {
10 | implementation(libs.androidx.annotation)
11 | }
12 |
--------------------------------------------------------------------------------
/logo/src/main/kotlin/dev/sanmer/logo/Logo.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.logo
2 |
3 | import androidx.annotation.DrawableRes
4 |
5 | data class Logo(
6 | @DrawableRes
7 | val res: Int,
8 | val name: String,
9 | val refillable: Boolean
10 | ) {
11 | companion object Default {
12 | private val logos = hashMapOf()
13 |
14 | private fun getOrNull(name: String): Logo? {
15 | if (logos.contains(name)) return logos[name]
16 |
17 | val brand = Brand.entries.firstOrNull {
18 | it.regex.toRegex().matches(name)
19 | }
20 | if (brand != null) {
21 | return Logo(
22 | res = brand.res,
23 | name = brand.name,
24 | refillable = false
25 | ).also { logos[name] = it }
26 | }
27 |
28 | val normal = Normal.entries.firstOrNull {
29 | it.regex.toRegex().containsMatchIn(name)
30 | }
31 | if (normal != null) {
32 | return Logo(
33 | res = normal.res,
34 | name = normal.name,
35 | refillable = true
36 | ).also { logos[name] = it }
37 | }
38 |
39 | logos[name] = null
40 | return null
41 | }
42 |
43 | fun getOrDefault(name: String) =
44 | getOrNull(name) ?: Logo(
45 | res = R.drawable.normal_default,
46 | name = name,
47 | refillable = true
48 | )
49 |
50 | fun getOr(name: String, default: () -> Logo) =
51 | getOrNull(name) ?: default()
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/logo/src/main/kotlin/dev/sanmer/logo/Normal.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.logo
2 |
3 | import androidx.annotation.DrawableRes
4 |
5 | enum class Normal(
6 | @DrawableRes
7 | val res: Int,
8 | internal val regex: String
9 | ) {
10 | Cloud(
11 | res = R.drawable.normal_cloud,
12 | regex = "(?i)Cloud"
13 | )
14 | }
--------------------------------------------------------------------------------
/logo/src/main/res/drawable-night/brand_apple.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable-night/brand_bybit.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
26 |
27 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable-night/brand_github.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable-night/brand_okx.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
26 |
27 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable-night/brand_x.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_aliyun.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
19 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_apple.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_binance.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
26 |
27 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_bybit.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
26 |
27 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_cloudflare.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
19 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_coinbase.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_digitalocean.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_discord.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
16 |
17 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_facebook.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_github.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_gitlab.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
23 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_google.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
23 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_jetbrains.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
17 |
20 |
23 |
26 |
29 |
30 |
31 |
32 |
36 |
40 |
41 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_mega.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_microsoft.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
23 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_okx.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
26 |
27 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_onlyfans.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_orcid.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
23 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_tencent_cloud.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
19 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_wise.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/brand_x.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/normal_cloud.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/logo/src/main/res/drawable/normal_default.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/logo/svg/aliyun.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
--------------------------------------------------------------------------------
/logo/svg/apple.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/logo/svg/binance.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/logo/svg/bybit.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/logo/svg/coinbase.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/logo/svg/digitalocean.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/logo/svg/discord.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/logo/svg/facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/logo/svg/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/logo/svg/gitlab.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/logo/svg/google.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
--------------------------------------------------------------------------------
/logo/svg/jetbrains.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/logo/svg/mega.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/logo/svg/microsoft.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/logo/svg/okx.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/logo/svg/onlyfans.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/logo/svg/orcid.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/logo/svg/tencent-cloud.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
--------------------------------------------------------------------------------
/logo/svg/wise.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/logo/svg/x.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
3 |
4 | dependencyResolutionManagement {
5 | repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
6 | repositories {
7 | google()
8 | mavenCentral()
9 | }
10 | }
11 |
12 | pluginManagement {
13 | includeBuild("build-logic")
14 | repositories {
15 | google()
16 | mavenCentral()
17 | gradlePluginPortal()
18 | }
19 | }
20 |
21 | rootProject.name = "Authenticator"
22 | include(":core", ":logo", ":app")
23 |
--------------------------------------------------------------------------------