diff --git a/.github/workflows/auth-release.yml b/.github/workflows/auth-release.yml index 174b6c1d3..cf3749ae6 100644 --- a/.github/workflows/auth-release.yml +++ b/.github/workflows/auth-release.yml @@ -85,30 +85,21 @@ jobs: - name: Install dependencies for desktop build run: | sudo apt-get update -y - sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi-dev libtiff5 + sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi-dev libtiff5 sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu' - - name: Install appimagetool - run: | - wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" - chmod +x appimagetool - mv appimagetool /usr/local/bin/ - - name: Build desktop app run: | flutter config --enable-linux-desktop dart pub global activate flutter_distributor flutter_distributor package --platform=linux --targets=deb --skip-clean - flutter_distributor package --platform=linux --targets=rpm --skip-clean - flutter_distributor package --platform=linux --targets=appimage --skip-clean mv dist/**/*-*-linux.deb artifacts/ente-${{ github.ref_name }}-x86_64.deb - mv dist/**/*-*-linux.rpm artifacts/ente-${{ github.ref_name }}-x86_64.rpm - mv dist/**/*-*-linux.AppImage artifacts/ente-${{ github.ref_name }}-x86_64.AppImage env: LIBSODIUM_USE_PKGCONFIG: 1 - - name: Generate checksums - run: sha256sum artifacts/ente-* > artifacts/sha256sum + - name: Generate checksums and push to artifacts + run: | + sha256sum artifacts/ente-* > artifacts/sha256sum-apk-deb - name: Create a draft GitHub release uses: ncipollo/release-action@v1 @@ -128,6 +119,61 @@ jobs: releaseFiles: auth/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab track: internal + build-fedora-etc: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: auth + + steps: + - name: Checkout code and submodules + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Flutter ${{ env.FLUTTER_VERSION }} + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Create artifacts directory + run: mkdir artifacts + + - name: Install dependencies for desktop build + run: | + sudo apt-get update -y + sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff5 + sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu' + + - name: Install appimagetool + run: | + wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" + chmod +x appimagetool + mv appimagetool /usr/local/bin/ + + - name: Build desktop app + run: | + flutter config --enable-linux-desktop + dart pub global activate flutter_distributor + flutter_distributor package --platform=linux --targets=rpm --skip-clean + flutter_distributor package --platform=linux --targets=appimage --skip-clean + mv dist/**/*-*-linux.rpm artifacts/ente-${{ github.ref_name }}-x86_64.rpm + mv dist/**/*-*-linux.AppImage artifacts/ente-${{ github.ref_name }}-x86_64.AppImage + + - name: Generate checksums + run: sha256sum artifacts/ente-* >> artifacts/sha256sum-rpm-appimage + + - name: Create a draft GitHub release + uses: ncipollo/release-action@v1 + with: + artifacts: "auth/artifacts/*" + draft: true + allowUpdates: true + updateOnlyUnreleased: true + build-windows: runs-on: windows-latest diff --git a/.gitignore b/.gitignore index 35ef93d42..8699b46ee 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ # macOS .DS_Store +.idea +.ente.authenticator.db +.ente.offline_authenticator.db diff --git a/auth/android/app/build.gradle b/auth/android/app/build.gradle index 5621b08b6..a0179af5b 100644 --- a/auth/android/app/build.gradle +++ b/auth/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,10 +22,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { @@ -32,7 +29,18 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 34 + namespace "io.ente.auth" + compileSdk 34 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -46,6 +54,8 @@ android { defaultConfig { applicationId "io.ente.auth" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion 21 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() @@ -105,13 +115,4 @@ flutter { source '../..' } -dependencies { - implementation 'io.sentry:sentry-android:2.0.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.android.support:multidex:1.0.3' - implementation 'com.google.guava:guava:28.2-android' - implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} +dependencies {} diff --git a/auth/android/app/src/debug/AndroidManifest.xml b/auth/android/app/src/debug/AndroidManifest.xml index 68e4e89c4..399f6981d 100644 --- a/auth/android/app/src/debug/AndroidManifest.xml +++ b/auth/android/app/src/debug/AndroidManifest.xml @@ -1,6 +1,6 @@ - - diff --git a/auth/android/app/src/main/AndroidManifest.xml b/auth/android/app/src/main/AndroidManifest.xml index abe72b565..a7b34f1ad 100644 --- a/auth/android/app/src/main/AndroidManifest.xml +++ b/auth/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ + xmlns:tools="http://schemas.android.com/tools"> - + - diff --git a/auth/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/auth/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index fbfe92399..000000000 Binary files a/auth/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/auth/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/auth/android/app/src/main/res/mipmap-hdpi/launcher_icon.png index 6fbcb6df9..be000c8b3 100644 Binary files a/auth/android/app/src/main/res/mipmap-hdpi/launcher_icon.png and b/auth/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/auth/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/auth/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 6105c4a2b..000000000 Binary files a/auth/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/auth/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/auth/android/app/src/main/res/mipmap-mdpi/launcher_icon.png index 13fdf3b88..f49d34bb5 100644 Binary files a/auth/android/app/src/main/res/mipmap-mdpi/launcher_icon.png and b/auth/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/auth/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/auth/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index b34272b61..000000000 Binary files a/auth/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/auth/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/auth/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png index 5f852e4a3..ef950b6e9 100644 Binary files a/auth/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png and b/auth/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/auth/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/auth/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index faa2e9c60..000000000 Binary files a/auth/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/auth/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/auth/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png index 5c82f386a..e97eba5d2 100644 Binary files a/auth/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png and b/auth/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/auth/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/auth/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 9814894c6..000000000 Binary files a/auth/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/auth/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/auth/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png index 3bea3482c..a37c745ae 100644 Binary files a/auth/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png and b/auth/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/auth/android/app/src/main/res/values-night-v31/styles.xml b/auth/android/app/src/main/res/values-night-v31/styles.xml index 2c379953f..c4a573dfe 100644 --- a/auth/android/app/src/main/res/values-night-v31/styles.xml +++ b/auth/android/app/src/main/res/values-night-v31/styles.xml @@ -4,7 +4,10 @@ diff --git a/auth/android/build.gradle b/auth/android/build.gradle index 47890036d..bc157bd1a 100644 --- a/auth/android/build.gradle +++ b/auth/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.22' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -21,6 +8,8 @@ allprojects { rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { project.evaluationDependsOn(':app') } diff --git a/auth/android/gradle.properties b/auth/android/gradle.properties index 94adc3a3f..598d13fee 100644 --- a/auth/android/gradle.properties +++ b/auth/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/auth/android/gradle/wrapper/gradle-wrapper.properties b/auth/android/gradle/wrapper/gradle-wrapper.properties index cc5527d78..e1ca574ef 100644 --- a/auth/android/gradle/wrapper/gradle-wrapper.properties +++ b/auth/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/auth/android/settings.gradle b/auth/android/settings.gradle index 44e62bcf0..748caceba 100644 --- a/auth/android/settings.gradle +++ b/auth/android/settings.gradle @@ -1,11 +1,26 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.8.22" apply false +} + +include ":app" diff --git a/auth/assets/generation-icons/icon-light-adaptive-bg.png b/auth/assets/generation-icons/icon-light-adaptive-bg.png new file mode 100644 index 000000000..d7bde2bdd Binary files /dev/null and b/auth/assets/generation-icons/icon-light-adaptive-bg.png differ diff --git a/auth/assets/generation-icons/icon-light-adaptive-fg.png b/auth/assets/generation-icons/icon-light-adaptive-fg.png index c3899f446..6c1121a49 100644 Binary files a/auth/assets/generation-icons/icon-light-adaptive-fg.png and b/auth/assets/generation-icons/icon-light-adaptive-fg.png differ diff --git a/auth/assets/generation-icons/icon-light.png b/auth/assets/generation-icons/icon-light.png index 5ef7b5a8a..cccf23a2c 100644 Binary files a/auth/assets/generation-icons/icon-light.png and b/auth/assets/generation-icons/icon-light.png differ diff --git a/auth/assets/splash-screen-dark.png b/auth/assets/splash-screen-dark.png deleted file mode 100644 index 5401a47ad..000000000 Binary files a/auth/assets/splash-screen-dark.png and /dev/null differ diff --git a/auth/assets/splash-screen-light.png b/auth/assets/splash-screen-light.png deleted file mode 100644 index a97df13b3..000000000 Binary files a/auth/assets/splash-screen-light.png and /dev/null differ diff --git a/auth/assets/splash/splash-icon-fg-12.png b/auth/assets/splash/splash-icon-fg-12.png new file mode 100644 index 000000000..1a82d32f2 Binary files /dev/null and b/auth/assets/splash/splash-icon-fg-12.png differ diff --git a/auth/assets/splash/splash-icon-fg.png b/auth/assets/splash/splash-icon-fg.png new file mode 100644 index 000000000..58139acb2 Binary files /dev/null and b/auth/assets/splash/splash-icon-fg.png differ diff --git a/auth/assets/svg/button-tint.svg b/auth/assets/svg/button-tint.svg new file mode 100644 index 000000000..1751aece1 --- /dev/null +++ b/auth/assets/svg/button-tint.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/auth/assets/svg/pin-active.svg b/auth/assets/svg/pin-active.svg new file mode 100644 index 000000000..3ba870f5d --- /dev/null +++ b/auth/assets/svg/pin-active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/auth/assets/svg/pin-card.svg b/auth/assets/svg/pin-card.svg new file mode 100644 index 000000000..59b6e15e4 --- /dev/null +++ b/auth/assets/svg/pin-card.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/auth/assets/svg/pin-inactive.svg b/auth/assets/svg/pin-inactive.svg new file mode 100644 index 000000000..2cc59a362 --- /dev/null +++ b/auth/assets/svg/pin-inactive.svg @@ -0,0 +1,3 @@ + + + diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 23ac5355e..c3d5e0675 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 233c57d84..92a287d03 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 8dfb32a97..73c2972e7 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 780cae73a..45a215602 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 09f8c298d..8a871c8e1 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index d198bb082..3655056e3 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 90060839d..3cdcbe923 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 8dfb32a97..73c2972e7 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index fe8e47ed3..7bf74dea0 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 14e9af73d..6cb3e22cd 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 000000000..8fb6f13c6 Binary files /dev/null and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 000000000..63c4f03db Binary files /dev/null and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 000000000..6ab8f0dc2 Binary files /dev/null and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 000000000..9d2b175ed Binary files /dev/null and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 14e9af73d..6cb3e22cd 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 21b297f8d..5c75eab74 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 000000000..f36ab4838 Binary files /dev/null and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 000000000..8dc12384b Binary files /dev/null and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index f7ef5fa1b..cccb2c4fe 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index e2ed1b283..1355c5b74 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 450115a34..15e1f2c68 100644 Binary files a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png new file mode 100644 index 000000000..f04fe3978 Binary files /dev/null and b/auth/ios/Runner/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json b/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json index fa3132785..8bb185b10 100644 --- a/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json +++ b/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json @@ -2,8 +2,7 @@ "images" : [ { "filename" : "background.png", - "idiom" : "universal", - "scale" : "1x" + "idiom" : "universal" }, { "appearances" : [ @@ -13,36 +12,7 @@ } ], "filename" : "darkbackground.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png b/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png index e29b3b59f..3107d37fa 100644 Binary files a/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png and b/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png differ diff --git a/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png b/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png index 1b5df34e7..71e9c817e 100644 Binary files a/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png and b/auth/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png differ diff --git a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json index f3387d4ae..00cabce83 100644 --- a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ b/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -5,48 +5,15 @@ "idiom" : "universal", "scale" : "1x" }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "LaunchImageDark.png", - "idiom" : "universal", - "scale" : "1x" - }, { "filename" : "LaunchImage@2x.png", "idiom" : "universal", "scale" : "2x" }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "LaunchImageDark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, { "filename" : "LaunchImage@3x.png", "idiom" : "universal", "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "LaunchImageDark@3x.png", - "idiom" : "universal", - "scale" : "3x" } ], "info" : { diff --git a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png index 899cecf22..91acb41ae 100644 Binary files a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and b/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png index 4bb7a5751..9a7c72afa 100644 Binary files a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and b/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png index 176f0c723..5b4d99582 100644 Binary files a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and b/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png b/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png deleted file mode 100644 index 87f84c70e..000000000 Binary files a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png and /dev/null differ diff --git a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png b/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png deleted file mode 100644 index ce01bec05..000000000 Binary files a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png and /dev/null differ diff --git a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png b/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png deleted file mode 100644 index 75f4b1f3c..000000000 Binary files a/auth/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png and /dev/null differ diff --git a/auth/ios/Runner/Base.lproj/LaunchScreen.storyboard b/auth/ios/Runner/Base.lproj/LaunchScreen.storyboard index 8d2b7d51a..9e6bc010b 100644 --- a/auth/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/auth/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -38,7 +38,7 @@ - + diff --git a/auth/ios/Runner/Info.plist b/auth/ios/Runner/Info.plist index 35921ba0c..f24fa7f9e 100644 --- a/auth/ios/Runner/Info.plist +++ b/auth/ios/Runner/Info.plist @@ -1,86 +1,86 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - auth - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLocalizations - - en - es - - CFBundleName - auth - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleURLSchemes - - otpauth - enteauth - - - - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - ITSAppUsesNonExemptEncryption - - LSRequiresIPhoneOS - - MinimumOSVersion - 12.0 - NSCameraUsageDescription - This app needs camera access to scan QR codes - NSFaceIDUsageDescription - Please allow auth to lock itself with FaceID or TouchID - NSPhotoLibraryUsageDescription - Please allow auth to pick a file to import data from - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIStatusBarHidden - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - LSSupportsOpeningDocumentsInPlace - - UIFileSharingEnabled - - + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + auth + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + en + es + + CFBundleName + auth + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleURLSchemes + + otpauth + enteauth + + + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + + LSRequiresIPhoneOS + + MinimumOSVersion + 12.0 + NSCameraUsageDescription + This app needs camera access to scan QR codes + NSFaceIDUsageDescription + Please allow auth to lock itself with FaceID or TouchID + NSPhotoLibraryUsageDescription + Please allow auth to pick a file to import data from + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + LSSupportsOpeningDocumentsInPlace + + UIFileSharingEnabled + + diff --git a/auth/lib/ente_theme_data.dart b/auth/lib/ente_theme_data.dart index 0316d014f..2eb19bf27 100644 --- a/auth/lib/ente_theme_data.dart +++ b/auth/lib/ente_theme_data.dart @@ -427,6 +427,10 @@ extension CustomColorScheme on ColorScheme { ? const Color.fromRGBO(246, 246, 246, 1) : const Color.fromRGBO(40, 40, 40, 0.6); + Color get primaryColor => brightness == Brightness.light + ? const Color(0xFF9610D6) + : const Color(0xFF9610D6); + EnteTheme get enteTheme => brightness == Brightness.light ? lightTheme : darkTheme; @@ -493,7 +497,7 @@ ElevatedButtonThemeData buildElevatedButtonThemeData({ ), padding: const EdgeInsets.symmetric(vertical: 18), shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8)), + borderRadius: BorderRadius.all(Radius.circular(4)), ), ), ); diff --git a/auth/lib/l10n/arb/app_en.arb b/auth/lib/l10n/arb/app_en.arb index c22bac930..e4d1a07a5 100644 --- a/auth/lib/l10n/arb/app_en.arb +++ b/auth/lib/l10n/arb/app_en.arb @@ -20,6 +20,8 @@ "codeIssuerHint": "Issuer", "codeSecretKeyHint": "Secret Key", "codeAccountHint": "Account (you@domain.com)", + "codeTagHint": "Tag", + "accountKeyType": "Type of key", "sessionExpired": "Session expired", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" @@ -156,6 +158,7 @@ } } }, + "invalidQRCode": "Invalid QR code", "noRecoveryKeyTitle": "No recovery key?", "enterEmailHint": "Enter your email address", "invalidEmailTitle": "Invalid email address", @@ -420,5 +423,18 @@ "invalidEndpoint": "Invalid endpoint", "invalidEndpointMessage": "Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again.", "endpointUpdatedMessage": "Endpoint updated successfully", - "customEndpoint": "Connected to {endpoint}" + "customEndpoint": "Connected to {endpoint}", + "pinText": "Pin", + "unpinText": "Unpin", + "pinnedCodeMessage": "{code} has been pinned", + "unpinnedCodeMessage": "{code} has been unpinned", + "tags": "Tags", + "createNewTag": "Create New Tag", + "tag": "Tag", + "create": "Create", + "editTag": "Edit Tag", + "deleteTagTitle": "Delete tag?", + "deleteTagMessage": "Are you sure you want to delete this tag? This action is irreversible.", + "somethingWentWrongParsingCode": "We were unable to parse {x} codes.", + "updateNotAvailable": "Update not available" } \ No newline at end of file diff --git a/auth/lib/main.dart b/auth/lib/main.dart index d8d22ca4f..9fa2841ff 100644 --- a/auth/lib/main.dart +++ b/auth/lib/main.dart @@ -17,6 +17,7 @@ import 'package:ente_auth/services/update_service.dart'; import 'package:ente_auth/services/user_remote_flag_service.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/services/window_listener_service.dart'; +import 'package:ente_auth/store/code_display_store.dart'; import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/ui/tools/app_lock.dart'; import 'package:ente_auth/ui/tools/lock_screen.dart'; @@ -145,6 +146,7 @@ Future _init(bool bool, {String? via}) async { await PreferenceService.instance.init(); await CodeStore.instance.init(); + await CodeDisplayStore.instance.init(); await Configuration.instance.init(); await Network.instance.init(); await UserService.instance.init(); @@ -157,7 +159,7 @@ Future _init(bool bool, {String? via}) async { } Future _setupPrivacyScreen() async { - if (!PlatformUtil.isMobile()) return; + if (!PlatformUtil.isMobile() || kDebugMode) return; final brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; bool isInDarkMode = brightness == Brightness.dark; diff --git a/auth/lib/models/code.dart b/auth/lib/models/code.dart index bd6077326..852d1dd78 100644 --- a/auth/lib/models/code.dart +++ b/auth/lib/models/code.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; + +import 'package:ente_auth/models/code_display.dart'; import 'package:ente_auth/utils/totp_util.dart'; class Code { @@ -13,10 +16,19 @@ class Code { final String secret; final Algorithm algorithm; final Type type; + + /// otpauth url in the code final String rawData; final int counter; bool? hasSynced; + final CodeDisplay display; + + bool get isPinned => display.pinned; + + final Object? err; + bool get hasError => err != null; + Code( this.account, this.issuer, @@ -28,8 +40,26 @@ class Code { this.counter, this.rawData, { this.generatedID, + required this.display, + this.err, }); + factory Code.withError(Object error, String rawData) { + return Code( + "", + "", + 0, + 0, + "", + Algorithm.sha1, + Type.totp, + 0, + rawData, + err: error, + display: CodeDisplay(), + ); + } + Code copyWith({ String? account, String? issuer, @@ -39,6 +69,7 @@ class Code { Algorithm? algorithm, Type? type, int? counter, + CodeDisplay? display, }) { final String updateAccount = account ?? this.account; final String updateIssuer = issuer ?? this.issuer; @@ -48,6 +79,7 @@ class Code { final Algorithm updatedAlgo = algorithm ?? this.algorithm; final Type updatedType = type ?? this.type; final int updatedCounter = counter ?? this.counter; + final CodeDisplay updatedDisplay = display ?? this.display; return Code( updateAccount, @@ -62,6 +94,7 @@ class Code { "&digits=$updatedDigits&issuer=$updateIssuer" "&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}", generatedID: generatedID, + display: updatedDisplay, ); } @@ -70,6 +103,7 @@ class Code { String account, String issuer, String secret, + CodeDisplay? display, int digits, ) { return Code( @@ -82,10 +116,11 @@ class Code { type, 0, "otpauth://${type.name}/$issuer:$account?algorithm=SHA1&digits=$digits&issuer=$issuer&period=30&secret=$secret", + display: display ?? CodeDisplay(), ); } - static Code fromRawData(String rawData) { + static Code fromOTPAuthUrl(String rawData, {CodeDisplay? display}) { Uri uri = Uri.parse(rawData); final issuer = _getIssuer(uri); @@ -100,12 +135,13 @@ class Code { _getType(uri), _getCounter(uri), rawData, + display: CodeDisplay.fromUri(uri) ?? CodeDisplay(), ); } catch (e) { // if account name contains # without encoding, // rest of the url are treated as url fragment if (rawData.contains("#")) { - return Code.fromRawData(rawData.replaceAll("#", '%23')); + return Code.fromOTPAuthUrl(rawData.replaceAll("#", '%23')); } else { rethrow; } @@ -129,6 +165,24 @@ class Code { } } + static Code fromExportJson(Map rawJson) { + Code resultCode = Code.fromOTPAuthUrl( + rawJson['rawData'], + display: CodeDisplay.fromJson(rawJson['display']), + ); + return resultCode; + } + + String toOTPAuthUrlFormat() { + final uri = Uri.parse(rawData); + final query = {...uri.queryParameters}; + query["codeDisplay"] = jsonEncode(display.toJson()); + + final newUri = uri.replace(queryParameters: query); + + return jsonEncode(newUri.toString()); + } + static String _getIssuer(Uri uri) { try { if (uri.queryParameters.containsKey("issuer")) { diff --git a/auth/lib/models/code_display.dart b/auth/lib/models/code_display.dart new file mode 100644 index 000000000..84deb916b --- /dev/null +++ b/auth/lib/models/code_display.dart @@ -0,0 +1,96 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; + +/// Used to store the display settings of a code. +class CodeDisplay { + final bool pinned; + final bool trashed; + final int lastUsedAt; + final int tapCount; + final List tags; + + CodeDisplay({ + this.pinned = false, + this.trashed = false, + this.lastUsedAt = 0, + this.tapCount = 0, + this.tags = const [], + }); + + // copyWith + CodeDisplay copyWith({ + bool? pinned, + bool? trashed, + int? lastUsedAt, + int? tapCount, + List? tags, + }) { + final bool updatedPinned = pinned ?? this.pinned; + final bool updatedTrashed = trashed ?? this.trashed; + final int updatedLastUsedAt = lastUsedAt ?? this.lastUsedAt; + final int updatedTapCount = tapCount ?? this.tapCount; + final List updatedTags = tags ?? this.tags; + + return CodeDisplay( + pinned: updatedPinned, + trashed: updatedTrashed, + lastUsedAt: updatedLastUsedAt, + tapCount: updatedTapCount, + tags: updatedTags, + ); + } + + factory CodeDisplay.fromJson(Map? json) { + if (json == null) { + return CodeDisplay(); + } + return CodeDisplay( + pinned: json['pinned'] ?? false, + trashed: json['trashed'] ?? false, + lastUsedAt: json['lastUsedAt'] ?? 0, + tapCount: json['tapCount'] ?? 0, + tags: List.from(json['tags'] ?? []), + ); + } + + static CodeDisplay? fromUri(Uri uri) { + if (!uri.queryParameters.containsKey("codeDisplay")) return null; + final String codeDisplay = + uri.queryParameters['codeDisplay']!.replaceAll('%2C', ','); + final decodedDisplay = jsonDecode(codeDisplay); + + return CodeDisplay.fromJson(decodedDisplay); + } + + Map toJson() { + return { + 'pinned': pinned, + 'trashed': trashed, + 'lastUsedAt': lastUsedAt, + 'tapCount': tapCount, + 'tags': tags, + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CodeDisplay && + other.pinned == pinned && + other.trashed == trashed && + other.lastUsedAt == lastUsedAt && + other.tapCount == tapCount && + listEquals(other.tags, tags); + } + + @override + int get hashCode { + return pinned.hashCode ^ + trashed.hashCode ^ + lastUsedAt.hashCode ^ + tapCount.hashCode ^ + tags.hashCode; + } +} diff --git a/auth/lib/onboarding/model/tag_enums.dart b/auth/lib/onboarding/model/tag_enums.dart new file mode 100644 index 000000000..6661b6770 --- /dev/null +++ b/auth/lib/onboarding/model/tag_enums.dart @@ -0,0 +1,10 @@ +enum TagChipState { + selected, + unselected, +} + +enum TagChipAction { + none, + menu, + check, +} diff --git a/auth/lib/onboarding/view/common/add_chip.dart b/auth/lib/onboarding/view/common/add_chip.dart new file mode 100644 index 000000000..39971f416 --- /dev/null +++ b/auth/lib/onboarding/view/common/add_chip.dart @@ -0,0 +1,26 @@ +import "package:ente_auth/theme/ente_theme.dart"; +import "package:flutter/material.dart"; + +class AddChip extends StatelessWidget { + final VoidCallback? onTap; + + const AddChip({ + super.key, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Icon( + Icons.add_circle_outline, + size: 30, + color: getEnteColorScheme(context).iconButtonColor, + ), + ), + ); + } +} diff --git a/auth/lib/onboarding/view/common/add_tag.dart b/auth/lib/onboarding/view/common/add_tag.dart new file mode 100644 index 000000000..3fb42071e --- /dev/null +++ b/auth/lib/onboarding/view/common/add_tag.dart @@ -0,0 +1,77 @@ +import "package:ente_auth/l10n/l10n.dart"; +import "package:flutter/material.dart"; + +class AddTagDialog extends StatefulWidget { + const AddTagDialog({ + super.key, + required this.onTap, + }); + + final void Function(String) onTap; + + @override + State createState() => _AddTagDialogState(); +} + +class _AddTagDialogState extends State { + String _tag = ""; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return AlertDialog( + title: Text(l10n.createNewTag), + content: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + decoration: InputDecoration( + hintText: l10n.tag, + hintStyle: const TextStyle( + color: Colors.white30, + ), + contentPadding: const EdgeInsets.all(12), + ), + onChanged: (value) { + setState(() { + _tag = value; + }); + }, + autocorrect: false, + initialValue: _tag, + autofocus: true, + ), + ], + ), + ), + actions: [ + TextButton( + child: Text( + l10n.cancel, + style: const TextStyle( + color: Colors.redAccent, + ), + ), + onPressed: () { + Navigator.pop(context); + }, + ), + TextButton( + child: Text( + l10n.create, + style: const TextStyle( + color: Colors.purple, + ), + ), + onPressed: () { + if (_tag.trim().isEmpty) return; + + widget.onTap(_tag); + }, + ), + ], + ); + } +} diff --git a/auth/lib/onboarding/view/common/edit_tag.dart b/auth/lib/onboarding/view/common/edit_tag.dart new file mode 100644 index 000000000..fe1f38564 --- /dev/null +++ b/auth/lib/onboarding/view/common/edit_tag.dart @@ -0,0 +1,89 @@ +import "package:ente_auth/l10n/l10n.dart"; +import 'package:ente_auth/store/code_display_store.dart'; +import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:flutter/material.dart'; + +class EditTagDialog extends StatefulWidget { + const EditTagDialog({ + super.key, + required this.tag, + }); + + final String tag; + + @override + State createState() => _EditTagDialogState(); +} + +class _EditTagDialogState extends State { + late String _tag = widget.tag; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return AlertDialog( + title: Text(l10n.editTag), + content: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + decoration: InputDecoration( + hintText: l10n.tag, + hintStyle: const TextStyle( + color: Colors.white30, + ), + contentPadding: const EdgeInsets.all(12), + ), + onChanged: (value) { + setState(() { + _tag = value; + }); + }, + autocorrect: false, + initialValue: _tag, + autofocus: true, + ), + ], + ), + ), + actions: [ + TextButton( + child: Text( + l10n.cancel, + style: const TextStyle( + color: Colors.redAccent, + ), + ), + onPressed: () { + Navigator.pop(context); + }, + ), + TextButton( + child: Text( + l10n.saveAction, + style: const TextStyle( + color: Colors.purple, + ), + ), + onPressed: () async { + if (_tag.trim().isEmpty) return; + + final dialog = createProgressDialog( + context, + context.l10n.pleaseWait, + ); + await dialog.show(); + + await CodeDisplayStore.instance.editTag(widget.tag, _tag); + + await dialog.hide(); + + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/auth/lib/onboarding/view/common/tag_chip.dart b/auth/lib/onboarding/view/common/tag_chip.dart new file mode 100644 index 000000000..7f71e68b8 --- /dev/null +++ b/auth/lib/onboarding/view/common/tag_chip.dart @@ -0,0 +1,132 @@ +import "package:ente_auth/l10n/l10n.dart"; +import "package:ente_auth/onboarding/model/tag_enums.dart"; +import "package:ente_auth/store/code_display_store.dart"; +import "package:ente_auth/theme/ente_theme.dart"; +import "package:flutter/material.dart"; +import "package:gradient_borders/box_borders/gradient_box_border.dart"; + +class TagChip extends StatelessWidget { + final String label; + final VoidCallback? onTap; + final TagChipState state; + final TagChipAction action; + + const TagChip({ + super.key, + required this.label, + this.state = TagChipState.unselected, + this.action = TagChipAction.none, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: state == TagChipState.selected + ? colorScheme.tagChipSelectedColor + : colorScheme.tagChipUnselectedColor, + borderRadius: BorderRadius.circular(100), + border: GradientBoxBorder( + gradient: LinearGradient( + colors: state == TagChipState.selected + ? colorScheme.tagChipSelectedGradient + : colorScheme.tagChipUnselectedGradient, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + ), + margin: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16) + .copyWith(right: 0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: TextStyle( + color: state == TagChipState.selected || + Theme.of(context).brightness == Brightness.dark + ? Colors.white + : colorScheme.tagTextUnselectedColor, + ), + ), + if (state == TagChipState.selected && + action == TagChipAction.check) ...[ + const SizedBox(width: 16), + const Icon( + Icons.check, + size: 16, + color: Colors.white, + ), + const SizedBox(width: 16), + ] else if (state == TagChipState.selected && + action == TagChipAction.menu) ...[ + SizedBox( + width: 48, + child: PopupMenuButton( + iconSize: 16, + padding: const EdgeInsets.symmetric(horizontal: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + surfaceTintColor: Theme.of(context).cardColor, + iconColor: Colors.white, + initialValue: -1, + onSelected: (value) { + if (value == 0) { + CodeDisplayStore.instance.showEditDialog(context, label); + } else if (value == 1) { + CodeDisplayStore.instance + .showDeleteTagDialog(context, label); + } + }, + itemBuilder: (BuildContext context) { + return [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.edit_outlined, size: 16), + const SizedBox(width: 12), + Text(context.l10n.edit), + ], + ), + value: 0, + ), + PopupMenuItem( + child: Row( + children: [ + Icon( + Icons.delete_outline, + size: 16, + color: colorScheme.deleteTagIconColor, + ), + const SizedBox(width: 12), + Text( + context.l10n.delete, + style: TextStyle( + color: colorScheme.deleteTagTextColor, + ), + ), + ], + ), + value: 1, + ), + ]; + }, + ), + ), + ] else ...[ + const SizedBox(width: 16), + ], + ], + ), + ), + ); + } +} diff --git a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart index 57edcc2e1..6741788c3 100644 --- a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart +++ b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart @@ -1,5 +1,15 @@ +import 'dart:async'; + +import 'package:ente_auth/core/event_bus.dart'; +import 'package:ente_auth/events/codes_updated_event.dart'; import "package:ente_auth/l10n/l10n.dart"; import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/models/code_display.dart'; +import 'package:ente_auth/onboarding/model/tag_enums.dart'; +import 'package:ente_auth/onboarding/view/common/add_chip.dart'; +import 'package:ente_auth/onboarding/view/common/add_tag.dart'; +import 'package:ente_auth/onboarding/view/common/tag_chip.dart'; +import 'package:ente_auth/store/code_display_store.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/components/models/button_result.dart'; import 'package:ente_auth/utils/dialog_util.dart'; @@ -21,6 +31,9 @@ class _SetupEnterSecretKeyPageState extends State { late TextEditingController _accountController; late TextEditingController _secretController; late bool _secretKeyObscured; + late List tags = [...?widget.code?.display.tags]; + List allTags = []; + StreamSubscription? _streamSubscription; @override void initState() { @@ -35,9 +48,26 @@ class _SetupEnterSecretKeyPageState extends State { text: widget.code?.secret, ); _secretKeyObscured = widget.code != null; + _loadTags(); + _streamSubscription = Bus.instance.on().listen((event) { + _loadTags(); + }); super.initState(); } + @override + void dispose() { + _streamSubscription?.cancel(); + super.dispose(); + } + + Future _loadTags() async { + allTags = await CodeDisplayStore.instance.getAllTags(); + if (mounted) { + setState(() {}); + } + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -50,6 +80,7 @@ class _SetupEnterSecretKeyPageState extends State { child: Padding( padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( // The validator receives the text that the user has entered. @@ -115,6 +146,65 @@ class _SetupEnterSecretKeyPageState extends State { controller: _accountController, ), const SizedBox(height: 40), + const SizedBox( + height: 20, + ), + Text( + l10n.tags, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + Wrap( + spacing: 12, + alignment: WrapAlignment.start, + children: [ + ...allTags.map( + (e) => TagChip( + label: e, + action: TagChipAction.check, + state: tags.contains(e) + ? TagChipState.selected + : TagChipState.unselected, + onTap: () { + if (tags.contains(e)) { + tags.remove(e); + } else { + tags.add(e); + } + setState(() {}); + }, + ), + ), + AddChip( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AddTagDialog( + onTap: (tag) { + if (allTags.contains(tag) && + tags.contains(tag)) { + return; + } + allTags.add(tag); + tags.add(tag); + setState(() {}); + Navigator.pop(context); + }, + ); + }, + barrierColor: Colors.black.withOpacity(0.85), + barrierDismissible: false, + ); + }, + ), + ], + ), + const SizedBox( + height: 40, + ), SizedBox( width: 400, child: OutlinedButton( @@ -134,13 +224,7 @@ class _SetupEnterSecretKeyPageState extends State { } await _saveCode(); }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 4, - ), - child: Text(l10n.saveAction), - ), + child: Text(l10n.saveAction), ), ), ], @@ -171,18 +255,22 @@ class _SetupEnterSecretKeyPageState extends State { return; } } + final CodeDisplay display = + widget.code?.display.copyWith(tags: tags) ?? CodeDisplay(tags: tags); final Code newCode = widget.code == null ? Code.fromAccountAndSecret( isStreamCode ? Type.steam : Type.totp, account, issuer, secret, + display, isStreamCode ? Code.steamDigits : Code.defaultDigits, ) : widget.code!.copyWith( account: account, issuer: issuer, secret: secret, + display: display, ); // Verify the validity of the code getOTP(newCode); diff --git a/auth/lib/store/code_display_store.dart b/auth/lib/store/code_display_store.dart new file mode 100644 index 000000000..74972f5a2 --- /dev/null +++ b/auth/lib/store/code_display_store.dart @@ -0,0 +1,112 @@ +import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/onboarding/view/common/edit_tag.dart'; +import 'package:ente_auth/services/authenticator_service.dart'; +import 'package:ente_auth/store/code_store.dart'; +import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:flutter/material.dart'; + +class CodeDisplayStore { + static final CodeDisplayStore instance = + CodeDisplayStore._privateConstructor(); + + CodeDisplayStore._privateConstructor(); + + late CodeStore _codeStore; + + Future init() async { + _codeStore = CodeStore.instance; + } + + Future> getAllTags({ + AccountMode? accountMode, + List? allCodes, + }) async { + final codes = allCodes ?? + await _codeStore.getAllCodes( + accountMode: accountMode, + sortCodes: false, + ); + final tags = {}; + for (final code in codes) { + if (code.hasError) continue; + tags.addAll(code.display.tags); + } + return tags.toList(); + } + + Future showDeleteTagDialog(BuildContext context, String tag) async { + FocusScope.of(context).requestFocus(); + final l10n = context.l10n; + + await showChoiceActionSheet( + context, + title: l10n.deleteTagTitle, + body: l10n.deleteTagMessage, + firstButtonLabel: l10n.delete, + isCritical: true, + firstButtonOnTap: () async { + // traverse through all the codes and edit this tag's value + final relevantCodes = await _getCodesByTag(tag); + + final tasks = []; + + for (final code in relevantCodes) { + final tags = code.display.tags; + tags.remove(tag); + tasks.add( + _codeStore.addCode( + code.copyWith( + display: code.display.copyWith(tags: tags), + ), + ), + ); + } + + await Future.wait(tasks); + }, + ); + } + + Future showEditDialog(BuildContext context, String tag) async { + await showDialog( + context: context, + builder: (BuildContext context) { + return EditTagDialog(tag: tag); + }, + barrierColor: Colors.black.withOpacity(0.85), + barrierDismissible: false, + ); + } + + Future> _getCodesByTag(String tag) async { + final codes = await _codeStore.getAllCodes(sortCodes: false); + return codes + .where( + (element) => !element.hasError && element.display.tags.contains(tag), + ) + .toList(); + } + + Future editTag(String previousTag, String updatedTag) async { + // traverse through all the codes and edit this tag's value + final relevantCodes = await _getCodesByTag(previousTag); + + final tasks = []; + + for (final code in relevantCodes) { + final tags = code.display.tags; + tags.remove(previousTag); + tags.add(updatedTag); + tasks.add( + CodeStore.instance.addCode( + code.copyWith( + display: code.display.copyWith(tags: tags), + ), + ), + ); + } + + await Future.wait(tasks); + } +} diff --git a/auth/lib/store/code_store.dart b/auth/lib/store/code_store.dart index 9b199f165..43d882281 100644 --- a/auth/lib/store/code_store.dart +++ b/auth/lib/store/code_store.dart @@ -22,27 +22,52 @@ class CodeStore { _authenticatorService = AuthenticatorService.instance; } - Future> getAllCodes({AccountMode? accountMode}) async { + Future> getAllCodes({ + AccountMode? accountMode, + bool sortCodes = true, + }) async { final mode = accountMode ?? _authenticatorService.getAccountMode(); final List entities = await _authenticatorService.getEntities(mode); final List codes = []; + for (final entity in entities) { - final decodeJson = jsonDecode(entity.rawData); - final code = Code.fromRawData(decodeJson); + late Code code; + try { + final decodeJson = jsonDecode(entity.rawData); + + if (decodeJson is String && decodeJson.startsWith('otpauth://')) { + code = Code.fromOTPAuthUrl(decodeJson); + } else { + code = Code.fromExportJson(decodeJson); + } + } catch (e) { + code = Code.withError(e, entity.rawData); + _logger.severe("Could not parse code", code.err); + } code.generatedID = entity.generatedID; code.hasSynced = entity.hasSynced; codes.add(code); } - // sort codes by issuer,account - codes.sort((a, b) { - final issuerComparison = compareAsciiLowerCaseNatural(a.issuer, b.issuer); - if (issuerComparison != 0) { - return issuerComparison; - } - return compareAsciiLowerCaseNatural(a.account, b.account); - }); + if (sortCodes) { + // sort codes by issuer,account + codes.sort((firstCode, secondCode) { + if (secondCode.isPinned && !firstCode.isPinned) return 1; + if (!secondCode.isPinned && firstCode.isPinned) return -1; + + final issuerComparison = + compareAsciiLowerCaseNatural(firstCode.issuer, secondCode.issuer); + if (issuerComparison != 0) { + return issuerComparison; + } + return compareAsciiLowerCaseNatural( + firstCode.account, + secondCode.account, + ); + }); + } + return codes; } @@ -52,30 +77,36 @@ class CodeStore { AccountMode? accountMode, }) async { final mode = accountMode ?? _authenticatorService.getAccountMode(); - final codes = await getAllCodes(accountMode: mode); + final allCodes = await getAllCodes(accountMode: mode); bool isExistingCode = false; - for (final existingCode in codes) { - if (existingCode == code) { - _logger.info("Found duplicate code, skipping add"); - return AddResult.duplicate; - } else if (existingCode.generatedID == code.generatedID) { + bool hasSameCode = false; + for (final existingCode in allCodes) { + if (existingCode.hasError) continue; + if (code.generatedID != null && + existingCode.generatedID == code.generatedID) { isExistingCode = true; break; } + if (existingCode == code) { + hasSameCode = true; + } + } + if (!isExistingCode && hasSameCode) { + return AddResult.duplicate; } late AddResult result; if (isExistingCode) { result = AddResult.updateCode; await _authenticatorService.updateEntry( code.generatedID!, - jsonEncode(code.rawData), + code.toOTPAuthUrlFormat(), shouldSync, mode, ); } else { result = AddResult.newCode; code.generatedID = await _authenticatorService.addEntry( - jsonEncode(code.rawData), + code.toOTPAuthUrlFormat(), shouldSync, mode, ); @@ -93,7 +124,7 @@ class CodeStore { bool _isOfflineImportRunning = false; Future importOfflineCodes() async { - if(_isOfflineImportRunning) { + if (_isOfflineImportRunning) { return; } _isOfflineImportRunning = true; @@ -107,8 +138,10 @@ class CodeStore { } logger.info('start import'); - List offlineCodes = await CodeStore.instance - .getAllCodes(accountMode: AccountMode.offline); + List offlineCodes = (await CodeStore.instance + .getAllCodes(accountMode: AccountMode.offline)) + .where((element) => !element.hasError) + .toList(); if (offlineCodes.isEmpty) { return; } @@ -117,8 +150,10 @@ class CodeStore { logger.info("skip as online sync is not done"); return; } - final List onlineCodes = - await CodeStore.instance.getAllCodes(accountMode: AccountMode.online); + final List onlineCodes = (await CodeStore.instance + .getAllCodes(accountMode: AccountMode.online)) + .where((element) => !element.hasError) + .toList(); logger.info( 'importing ${offlineCodes.length} offline codes with ${onlineCodes.length} online codes', ); diff --git a/auth/lib/theme/colors.dart b/auth/lib/theme/colors.dart index 9ac9d2d7e..278c00777 100644 --- a/auth/lib/theme/colors.dart +++ b/auth/lib/theme/colors.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; class EnteColorScheme { @@ -41,6 +39,8 @@ class EnteColorScheme { final Color primary400; final Color primary300; + final Color iconButtonColor; + final Color warning700; final Color warning500; final Color warning400; @@ -48,6 +48,28 @@ class EnteColorScheme { final Color caution500; final List avatarColors; + + // Tags + final Color tagChipSelectedColor; + final Color tagChipUnselectedColor; + final List tagChipSelectedGradient; + final List tagChipUnselectedGradient; + final Color tagTextUnselectedColor; + final Color deleteTagIconColor; + final Color deleteTagTextColor; + + // Code Widget + final Color errorCodeProgressColor; + final Color infoIconColor; + final Color errorCardTextColor; + final Color deleteCodeTextColor; + final List pinnedCardBoxShadow; + final Color pinnedBgColor; + + // Gradient Button + final Color gradientButtonBgColor; + final List gradientButtonBgColors; + const EnteColorScheme( this.backgroundBase, this.backgroundElevated, @@ -70,7 +92,23 @@ class EnteColorScheme { this.blurStrokeBase, this.blurStrokeFaint, this.blurStrokePressed, - this.avatarColors, { + this.avatarColors, + this.iconButtonColor, + this.tagChipUnselectedColor, + this.tagChipSelectedGradient, + this.tagChipUnselectedGradient, + this.pinnedBgColor, { + this.tagChipSelectedColor = _tagChipSelectedColor, + this.tagTextUnselectedColor = _tagTextUnselectedColor, + this.deleteTagIconColor = _deleteTagIconColor, + this.deleteTagTextColor = _deleteTagTextColor, + this.errorCodeProgressColor = _errorCodeProgressColor, + this.infoIconColor = _infoIconColor, + this.errorCardTextColor = _errorCardTextColor, + this.deleteCodeTextColor = _deleteCodeTextColor, + this.pinnedCardBoxShadow = _pinnedCardBoxShadow, + this.gradientButtonBgColor = _gradientButtonBgColor, + this.gradientButtonBgColors = _gradientButtonBgColors, this.primaryGreen = _primaryGreen, this.primary700 = _primary700, this.primary500 = _primary500, @@ -107,6 +145,11 @@ const EnteColorScheme lightScheme = EnteColorScheme( blurStrokeFaintLight, blurStrokePressedLight, avatarLight, + _iconButtonBrightColor, + _tagChipUnselectedColorLight, + _tagChipSelectedGradientLight, + _tagChipUnselectedGradientLight, + _pinnedBgColorLight, ); const EnteColorScheme darkScheme = EnteColorScheme( @@ -132,6 +175,11 @@ const EnteColorScheme darkScheme = EnteColorScheme( blurStrokeFaintDark, blurStrokePressedDark, avatarDark, + _iconButtonDarkColor, + _tagChipUnselectedColorDark, + _tagChipSelectedGradientDark, + _tagChipUnselectedGradientDark, + _pinnedBgColorDark, ); // Background Colors @@ -200,7 +248,10 @@ const Color _primary500 = Color.fromARGB(255, 204, 10, 101); const Color _primary400 = Color.fromARGB(255, 122, 41, 193); const Color _primary300 = Color.fromARGB(255, 152, 77, 244); -const Color _warning700 = Color.fromRGBO(234, 63, 63, 1); +const Color _iconButtonBrightColor = Color.fromRGBO(130, 50, 225, 1); +const Color _iconButtonDarkColor = Color.fromRGBO(255, 150, 16, 1); + +const Color _warning700 = Color.fromRGBO(245, 52, 52, 1); const Color _warning500 = Color.fromRGBO(255, 101, 101, 1); const Color _warning800 = Color(0xFFF53434); const Color warning500 = Color.fromRGBO(255, 101, 101, 1); @@ -260,3 +311,64 @@ const List avatarDark = [ Color.fromRGBO(209, 132, 132, 1), Color.fromRGBO(120, 181, 167, 1), ]; + +// Tags +const Color _tagChipUnselectedColorLight = Color(0xFFFCF5FF); +const Color _tagChipUnselectedColorDark = Color(0xFF1C0F22); +const List _tagChipUnselectedGradientLight = [ + Color(0x33AD00FF), + Color(0x338609C2), +]; +const List _tagChipUnselectedGradientDark = [ + Color(0xFFAD00FF), + Color(0x87A269BD), +]; +const Color _tagChipSelectedColor = Color(0xFF722ED1); +const List _tagChipSelectedGradientLight = [ + Color(0xFFB37FEB), + Color(0xFFAE40E3), +]; +const List _tagChipSelectedGradientDark = [ + Color(0xFFB37FEB), + Color(0x87AE40E3), +]; +const Color _tagTextUnselectedColor = Color(0xFF8232E1); +const Color _deleteTagIconColor = Color(0xFFF53434); +const Color _deleteTagTextColor = Color(0xFFF53434); + +// Code Widget +const Color _pinnedBgColorLight = Color(0xFFF9ECFF); +const Color _pinnedBgColorDark = Color(0xFF390C4F); +const Color _errorCodeProgressColor = Color(0xFFF53434); +const Color _infoIconColor = Color(0xFFF53434); +const Color _errorCardTextColor = Color(0xFFF53434); +const Color _deleteCodeTextColor = Color(0xFFFE4A49); +const List _pinnedCardBoxShadow = [ + BoxShadow( + color: Color(0x08000000), + blurRadius: 2, + offset: Offset(0, 7), + ), + BoxShadow( + color: Color(0x17000000), + blurRadius: 2, + offset: Offset(0, 4), + ), + BoxShadow( + color: Color(0x29000000), + blurRadius: 1, + offset: Offset(0, 1), + ), + BoxShadow( + color: Color(0x2E000000), + blurRadius: 1, + offset: Offset(0, 0), + ), +]; + +// Gradient Button +const Color _gradientButtonBgColor = Color(0xFF531DAB); +const List _gradientButtonBgColors = [ + Color(0xFFB37FEB), + Color(0xFF22075E), +]; diff --git a/auth/lib/ui/code_error_widget.dart b/auth/lib/ui/code_error_widget.dart new file mode 100644 index 000000000..ec532ccba --- /dev/null +++ b/auth/lib/ui/code_error_widget.dart @@ -0,0 +1,111 @@ +import 'package:ente_auth/ente_theme_data.dart'; +import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/theme/ente_theme.dart'; +import 'package:ente_auth/ui/common/gradient_button.dart'; +import 'package:ente_auth/ui/linear_progress_widget.dart'; +import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:flutter/material.dart'; + +class CodeErrorWidget extends StatelessWidget { + const CodeErrorWidget({ + super.key, + required this.errors, + }); + + final int errors; + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + + return Container( + height: 132, + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.codeCardBackgroundColor, + borderRadius: BorderRadius.circular(8), + ), + margin: const EdgeInsets.only( + left: 16, + right: 16, + bottom: 8, + top: 8, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 3, + child: LinearProgressWidget( + color: colorScheme.errorCodeProgressColor, + fractionOfStorage: 1, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + const SizedBox(width: 8), + Align( + alignment: Alignment.center, + child: Icon( + Icons.info, + size: 18, + color: colorScheme.infoIconColor, + ), + ), + const SizedBox(width: 8), + Text( + context.l10n.error, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: colorScheme.errorCardTextColor, + ), + ), + ], + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + context.l10n.somethingWentWrongParsingCode(errors), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + width: 102, + height: 28, + child: GradientButton( + text: context.l10n.contactSupport, + fontSize: 10, + onTap: () async { + await showErrorDialog( + context, + context.l10n.contactSupport, + context.l10n + .contactSupportViaEmailMessage("support@ente.io"), + ); + }, + borderWidth: 0.6, + borderRadius: 6, + ), + ), + const SizedBox(width: 6), + ], + ), + const SizedBox(height: 12), + ], + ), + ), + ); + } +} diff --git a/auth/lib/ui/code_timer_progress.dart b/auth/lib/ui/code_timer_progress.dart index b524a0c23..a215f0ca0 100644 --- a/auth/lib/ui/code_timer_progress.dart +++ b/auth/lib/ui/code_timer_progress.dart @@ -1,3 +1,4 @@ +import 'package:ente_auth/theme/ente_theme.dart'; import 'package:ente_auth/ui/linear_progress_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -47,9 +48,14 @@ class _CodeTimerProgressState extends State @override Widget build(BuildContext context) { - return LinearProgressWidget( - color: _progress > 0.4 ? Colors.green : Colors.orange, - fractionOfStorage: _progress, + return SizedBox( + height: 3, + child: LinearProgressWidget( + color: _progress > 0.4 + ? getEnteColorScheme(context).primary700 + : Colors.orange, + fractionOfStorage: _progress, + ), ); } } diff --git a/auth/lib/ui/code_widget.dart b/auth/lib/ui/code_widget.dart index d989edf18..7a9eae46f 100644 --- a/auth/lib/ui/code_widget.dart +++ b/auth/lib/ui/code_widget.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:ui' as ui; import 'package:clipboard/clipboard.dart'; import 'package:ente_auth/core/configuration.dart'; @@ -11,6 +12,7 @@ import 'package:ente_auth/onboarding/view/view_qr_page.dart'; import 'package:ente_auth/services/local_authentication_service.dart'; import 'package:ente_auth/services/preference_service.dart'; import 'package:ente_auth/store/code_store.dart'; +import 'package:ente_auth/theme/ente_theme.dart'; import 'package:ente_auth/ui/code_timer_progress.dart'; import 'package:ente_auth/ui/utils/icon_utils.dart'; import 'package:ente_auth/utils/dialog_util.dart'; @@ -20,13 +22,17 @@ import 'package:ente_auth/utils/totp_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:logging/logging.dart'; import 'package:move_to_background/move_to_background.dart'; class CodeWidget extends StatefulWidget { final Code code; - const CodeWidget(this.code, {super.key}); + const CodeWidget( + this.code, { + super.key, + }); @override State createState() => _CodeWidgetState(); @@ -42,6 +48,7 @@ class _CodeWidgetState extends State { late bool _shouldShowLargeIcon; late bool _hideCode; bool isMaskingEnabled = false; + late final colorScheme = getEnteColorScheme(context); @override void initState() { @@ -97,6 +104,13 @@ class _CodeWidgetState extends State { icon: Icons.qr_code_2_outlined, onSelected: () => _onShowQrPressed(null), ), + MenuItem( + label: widget.code.isPinned ? l10n.unpinText : l10n.pinText, + icon: widget.code.isPinned + ? Icons.push_pin + : Icons.push_pin_outlined, + onSelected: () => _onPinPressed(null), + ), MenuItem( label: l10n.edit, icon: Icons.edit, @@ -119,16 +133,16 @@ class _CodeWidgetState extends State { return Slidable( key: ValueKey(widget.code.hashCode), endActionPane: ActionPane( - extentRatio: 0.60, + extentRatio: 0.90, motion: const ScrollMotion(), children: [ const SizedBox( - width: 4, + width: 14, ), SlidableAction( onPressed: _onShowQrPressed, backgroundColor: Colors.grey.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), + borderRadius: const BorderRadius.all(Radius.circular(8)), foregroundColor: Theme.of(context).colorScheme.inverseBackgroundColor, icon: Icons.qr_code_2_outlined, @@ -137,12 +151,48 @@ class _CodeWidgetState extends State { spacing: 8, ), const SizedBox( - width: 4, + width: 14, + ), + CustomSlidableAction( + onPressed: _onPinPressed, + backgroundColor: Colors.grey.withOpacity(0.1), + borderRadius: const BorderRadius.all(Radius.circular(8)), + foregroundColor: + Theme.of(context).colorScheme.inverseBackgroundColor, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (widget.code.isPinned) + SvgPicture.asset( + "assets/svg/pin-active.svg", + colorFilter: ui.ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ) + else + SvgPicture.asset( + "assets/svg/pin-inactive.svg", + colorFilter: ui.ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + const SizedBox(height: 8), + Text( + widget.code.isPinned ? l10n.unpinText : l10n.pinText, + ), + ], + ), + padding: const EdgeInsets.only(left: 4, right: 0), + ), + const SizedBox( + width: 14, ), SlidableAction( onPressed: _onEditPressed, backgroundColor: Colors.grey.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), + borderRadius: const BorderRadius.all(Radius.circular(8)), foregroundColor: Theme.of(context).colorScheme.inverseBackgroundColor, icon: Icons.edit_outlined, @@ -151,13 +201,13 @@ class _CodeWidgetState extends State { spacing: 8, ), const SizedBox( - width: 4, + width: 14, ), SlidableAction( onPressed: _onDeletePressed, backgroundColor: Colors.grey.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - foregroundColor: const Color(0xFFFE4A49), + borderRadius: const BorderRadius.all(Radius.circular(8)), + foregroundColor: colorScheme.deleteCodeTextColor, icon: Icons.delete, label: l10n.delete, padding: const EdgeInsets.only(left: 0, right: 0), @@ -175,10 +225,15 @@ class _CodeWidgetState extends State { } Widget _clippedCard(AppLocalizations l10n) { - return ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Container( + return Container( + height: 132, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), color: Theme.of(context).colorScheme.codeCardBackgroundColor, + boxShadow: widget.code.isPinned ? colorScheme.pinnedCardBoxShadow : [], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), child: Material( color: Colors.transparent, child: InkWell( @@ -208,37 +263,56 @@ class _CodeWidgetState extends State { } Widget _getCardContents(AppLocalizations l10n) { - return SizedBox( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (widget.code.type.isTOTPCompatible) - CodeTimerProgress( - period: widget.code.period, - ), - const SizedBox( - height: 16, - ), - Row( - children: [ - _shouldShowLargeIcon ? _getIcon() : const SizedBox.shrink(), - Expanded( - child: Column( - children: [ - _getTopRow(), - const SizedBox(height: 4), - _getBottomRow(l10n), - ], - ), + return Stack( + children: [ + if (widget.code.isPinned) + Align( + alignment: Alignment.topRight, + child: CustomPaint( + painter: PinBgPainter( + color: colorScheme.pinnedBgColor, ), - ], + size: const Size(39, 39), + ), ), - const SizedBox( - height: 20, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (widget.code.type == Type.totp) + CodeTimerProgress( + period: widget.code.period, + ), + const SizedBox(height: 16), + Row( + children: [ + _shouldShowLargeIcon ? _getIcon() : const SizedBox.shrink(), + Expanded( + child: Column( + children: [ + _getTopRow(), + const SizedBox(height: 4), + _getBottomRow(l10n), + ], + ), + ), + ], + ), + const SizedBox( + height: 20, + ), + ], + ), + if (widget.code.isPinned) ...[ + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.only(right: 6, top: 6), + child: SvgPicture.asset("assets/svg/pin-card.svg"), + ), ), ], - ), + ], ); } @@ -422,7 +496,9 @@ class _CodeWidgetState extends State { final Code? code = await Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { - return SetupEnterSecretKeyPage(code: widget.code); + return SetupEnterSecretKeyPage( + code: widget.code, + ); }, ), ); @@ -448,6 +524,24 @@ class _CodeWidgetState extends State { ); } + Future _onPinPressed(_) async { + bool currentlyPinned = widget.code.isPinned; + final display = widget.code.display; + final Code code = widget.code.copyWith( + display: display.copyWith(pinned: !currentlyPinned), + ); + unawaited( + CodeStore.instance.addCode(code).then( + (value) => showToast( + context, + !currentlyPinned + ? context.l10n.pinnedCodeMessage(widget.code.issuer) + : context.l10n.unpinnedCodeMessage(widget.code.issuer), + ), + ), + ); + } + void _onDeletePressed(_) async { bool isAuthSuccessful = await LocalAuthenticationService.instance.requestLocalAuthentication( @@ -499,3 +593,36 @@ class _CodeWidgetState extends State { return code; } } + +class PinBgPainter extends CustomPainter { + final Color color; + final PaintingStyle paintingStyle; + + PinBgPainter({ + this.color = Colors.black, + this.paintingStyle = PaintingStyle.fill, + }); + + @override + void paint(Canvas canvas, Size size) { + Paint paint = Paint() + ..color = color + ..style = paintingStyle; + + canvas.drawPath(getTrianglePath(size.width, size.height), paint); + } + + Path getTrianglePath(double x, double y) { + return Path() + ..moveTo(0, 0) + ..lineTo(x, 0) + ..lineTo(x, y) + ..lineTo(0, 0); + } + + @override + bool shouldRepaint(PinBgPainter oldDelegate) { + return oldDelegate.color != color || + oldDelegate.paintingStyle != paintingStyle; + } +} diff --git a/auth/lib/ui/common/gradient_button.dart b/auth/lib/ui/common/gradient_button.dart index 8a24c6832..436e1bfb9 100644 --- a/auth/lib/ui/common/gradient_button.dart +++ b/auth/lib/ui/common/gradient_button.dart @@ -1,7 +1,9 @@ +import 'package:ente_auth/theme/ente_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:gradient_borders/box_borders/gradient_box_border.dart'; -class GradientButton extends StatelessWidget { - final List linearGradientColors; +class GradientButton extends StatefulWidget { final Function? onTap; // text is ignored if child is specified @@ -13,33 +15,39 @@ class GradientButton extends StatelessWidget { // padding between the text and icon final double paddingValue; - // used when two icons are in row - final bool reversedGradient; + final double fontSize; + final double borderRadius; + final double borderWidth; const GradientButton({ super.key, - this.linearGradientColors = const [ - Color.fromARGB(255, 133, 44, 210), - Color.fromARGB(255, 187, 26, 93), - ], - this.reversedGradient = false, this.onTap, this.text = '', this.iconData, this.paddingValue = 0.0, + this.fontSize = 18, + this.borderRadius = 4, + this.borderWidth = 1, }); + @override + State createState() => _GradientButtonState(); +} + +class _GradientButtonState extends State { + bool isTapped = false; + @override Widget build(BuildContext context) { Widget buttonContent; - if (iconData == null) { + if (widget.iconData == null) { buttonContent = Text( - text, - style: const TextStyle( + widget.text, + style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600, fontFamily: 'Inter-SemiBold', - fontSize: 18, + fontSize: widget.fontSize, ), ); } else { @@ -48,38 +56,79 @@ class GradientButton extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( - iconData, + widget.iconData, size: 20, color: Colors.white, ), const Padding(padding: EdgeInsets.symmetric(horizontal: 6)), Text( - text, - style: const TextStyle( + widget.text, + style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600, fontFamily: 'Inter-SemiBold', - fontSize: 18, + fontSize: widget.fontSize, ), ), ], ); } + final colorScheme = getEnteColorScheme(context); + return InkWell( - onTap: onTap as void Function()?, - child: Container( - height: 56, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: const Alignment(0.1, -0.9), - end: const Alignment(-0.6, 0.9), - colors: reversedGradient - ? linearGradientColors.reversed.toList() - : linearGradientColors, + onTapDown: (_) { + setState(() { + isTapped = true; + }); + }, + onTapUp: (_) { + setState(() { + isTapped = false; + }); + }, + onTapCancel: () { + setState(() { + isTapped = false; + }); + }, + borderRadius: BorderRadius.circular(widget.borderRadius), + onTap: widget.onTap as void Function()?, + child: Stack( + children: [ + Container( + height: 56, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.borderRadius), + color: colorScheme.gradientButtonBgColor, + ), ), - borderRadius: BorderRadius.circular(8), - ), - child: Center(child: buttonContent), + if (!isTapped) + ClipRRect( + borderRadius: BorderRadius.circular(widget.borderRadius), + child: SvgPicture.asset( + 'assets/svg/button-tint.svg', + fit: BoxFit.fill, + width: double.infinity, + height: 56, + ), + ), + Container( + height: 56, + decoration: BoxDecoration( + border: GradientBoxBorder( + width: widget.borderWidth, + gradient: LinearGradient( + colors: colorScheme.gradientButtonBgColors, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + borderRadius: BorderRadius.circular(widget.borderRadius), + ), + child: Center(child: buttonContent), + ), + ], ), ); } diff --git a/auth/lib/ui/home_page.dart b/auth/lib/ui/home_page.dart index c3397d79a..4110a5f88 100644 --- a/auth/lib/ui/home_page.dart +++ b/auth/lib/ui/home_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:app_links/app_links.dart'; +import 'package:collection/collection.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/event_bus.dart'; import 'package:ente_auth/ente_theme_data.dart'; @@ -10,11 +11,15 @@ import 'package:ente_auth/events/icons_changed_event.dart'; import 'package:ente_auth/events/trigger_logout_event.dart'; import "package:ente_auth/l10n/l10n.dart"; import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/onboarding/model/tag_enums.dart'; +import 'package:ente_auth/onboarding/view/common/tag_chip.dart'; import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart'; import 'package:ente_auth/services/preference_service.dart'; import 'package:ente_auth/services/user_service.dart'; +import 'package:ente_auth/store/code_display_store.dart'; import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/ui/account/logout_dialog.dart'; +import 'package:ente_auth/ui/code_error_widget.dart'; import 'package:ente_auth/ui/code_widget.dart'; import 'package:ente_auth/ui/common/loading_widget.dart'; import 'package:ente_auth/ui/home/coach_mark_widget.dart'; @@ -54,11 +59,13 @@ class _HomePageState extends State { final FocusNode searchInputFocusNode = FocusNode(); bool _showSearchBox = false; String _searchText = ""; - List _codes = []; + List? _allCodes; + List tags = []; List _filteredCodes = []; StreamSubscription? _streamSubscription; StreamSubscription? _triggerLogoutEvent; StreamSubscription? _iconsChangedEvent; + String selectedTag = ""; @override void initState() { @@ -96,14 +103,26 @@ class _HomePageState extends State { void _loadCodes() { CodeStore.instance.getAllCodes().then((codes) { - _codes = codes; - _hasLoaded = true; - _applyFilteringAndRefresh(); + _allCodes = codes; + + CodeDisplayStore.instance.getAllTags(allCodes: _allCodes).then((value) { + tags = value; + + if (mounted) { + if (!tags.contains(selectedTag)) { + selectedTag = ""; + } + _hasLoaded = true; + _applyFilteringAndRefresh(); + } + }); + }).onError((error, stackTrace) { + _logger.severe('Error while loading codes', error, stackTrace); }); } void _applyFilteringAndRefresh() { - if (_searchText.isNotEmpty && _showSearchBox) { + if (_searchText.isNotEmpty && _showSearchBox && _allCodes != null) { final String val = _searchText.toLowerCase(); // Prioritize issuer match above account for better UX while searching // for a specific TOTP for email providers. Searching for "emailProvider" like (gmail, proton) should @@ -112,17 +131,31 @@ class _HomePageState extends State { final List issuerMatch = []; final List accountMatch = []; - for (final Code code in _codes) { - if (code.issuer.toLowerCase().contains(val)) { - issuerMatch.add(code); - } else if (code.account.toLowerCase().contains(val)) { - accountMatch.add(code); + for (final Code codeState in _allCodes!) { + if (codeState.hasError || + selectedTag != "" && + !codeState.display.tags.contains(selectedTag)) { + continue; + } + + if (codeState.issuer.toLowerCase().contains(val)) { + issuerMatch.add(codeState); + } else if (codeState.account.toLowerCase().contains(val)) { + accountMatch.add(codeState); } } _filteredCodes = issuerMatch; _filteredCodes.addAll(accountMatch); } else { - _filteredCodes = _codes; + _filteredCodes = _allCodes + ?.where( + (element) => + !element.hasError && + (selectedTag == "" || + element.display.tags.contains(selectedTag)), + ) + .toList() ?? + []; } if (mounted) { setState(() {}); @@ -149,7 +182,7 @@ class _HomePageState extends State { if (code != null) { await CodeStore.instance.addCode(code); // Focus the new code by searching - if (_codes.length > 2) { + if ((_allCodes?.where((e) => !e.hasError).length ?? 0) > 2) { _focusNewCode(code); } } @@ -171,6 +204,7 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; + return PopScope( onPopInvoked: (_) async { if (_isSettingsOpen) { @@ -217,6 +251,7 @@ class _HomePageState extends State { focusedBorder: InputBorder.none, ), ), + centerTitle: true, actions: [ IconButton( icon: _showSearchBox @@ -241,7 +276,7 @@ class _HomePageState extends State { ], ), floatingActionButton: !_hasLoaded || - _codes.isEmpty || + (_allCodes?.isEmpty ?? true) || !PreferenceService.instance.hasShownCoachMark() ? null : _getFab(), @@ -258,18 +293,86 @@ class _HomePageState extends State { onManuallySetupTap: _redirectToManualEntryPage, ); } else { - final list = AlignedGridView.count( - crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400) - .clamp(1, double.infinity) - .toInt(), - itemBuilder: ((context, index) { - try { - return ClipRect(child: CodeWidget(_filteredCodes[index])); - } catch (e) { - return const Text("Failed"); - } - }), - itemCount: _filteredCodes.length, + final anyCodeHasError = + _allCodes?.firstWhereOrNull((element) => element.hasError) != null; + final indexOffset = anyCodeHasError ? 1 : 0; + + final list = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!anyCodeHasError) + SizedBox( + height: 48, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 2), + separatorBuilder: (context, index) => + const SizedBox(width: 8), + itemCount: tags.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return TagChip( + label: "All", + state: selectedTag == "" + ? TagChipState.selected + : TagChipState.unselected, + onTap: () { + selectedTag = ""; + setState(() {}); + _applyFilteringAndRefresh(); + }, + ); + } + return TagChip( + label: tags[index - 1], + action: TagChipAction.menu, + state: selectedTag == tags[index - 1] + ? TagChipState.selected + : TagChipState.unselected, + onTap: () { + if (selectedTag == tags[index - 1]) { + selectedTag = ""; + setState(() {}); + _applyFilteringAndRefresh(); + return; + } + selectedTag = tags[index - 1]; + setState(() {}); + _applyFilteringAndRefresh(); + }, + ); + }, + ), + ), + Expanded( + child: AlignedGridView.count( + crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400) + .clamp(1, double.infinity) + .toInt(), + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only(bottom: 80), + itemBuilder: ((context, index) { + if (index == 0 && anyCodeHasError) { + return CodeErrorWidget( + errors: _allCodes + ?.where((element) => element.hasError) + .length ?? + 0, + ); + } + final newIndex = index - indexOffset; + + return ClipRect( + child: CodeWidget( + _filteredCodes[newIndex], + ), + ); + }), + itemCount: _filteredCodes.length + indexOffset, + ), + ), + ], ); if (!PreferenceService.instance.hasShownCoachMark()) { return Stack( @@ -288,22 +391,12 @@ class _HomePageState extends State { (MediaQuery.sizeOf(context).width ~/ 400) .clamp(1, double.infinity) .toInt(), + padding: const EdgeInsets.only(bottom: 80), itemBuilder: ((context, index) { - Code? code; - try { - code = _filteredCodes[index]; - return CodeWidget(code); - } catch (e, s) { - _logger.severe("code widget error", e, s); - return Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - l10n.sorryUnableToGenCode(code?.issuer ?? ""), - ), - ), - ); - } + final codeState = _filteredCodes[index]; + return CodeWidget( + codeState, + ); }), itemCount: _filteredCodes.length, ) @@ -360,7 +453,7 @@ class _HomePageState extends State { } if (mounted && link.toLowerCase().startsWith("otpauth://")) { try { - final newCode = Code.fromRawData(link); + final newCode = Code.fromOTPAuthUrl(link); getNextTotp(newCode); CodeStore.instance.addCode(newCode); _focusNewCode(newCode); diff --git a/auth/lib/ui/scanner_page.dart b/auth/lib/ui/scanner_page.dart index 6a7793631..a0f88b7c8 100644 --- a/auth/lib/ui/scanner_page.dart +++ b/auth/lib/ui/scanner_page.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; @@ -66,11 +67,12 @@ class ScannerPageState extends State { } controller.scannedDataStream.listen((scanData) { try { - final code = Code.fromRawData(scanData.code!); + final code = Code.fromOTPAuthUrl(scanData.code!); controller.dispose(); Navigator.of(context).pop(code); } catch (e) { // Log + showToast(context, context.l10n.invalidQRCode); } }); } diff --git a/auth/lib/ui/settings/data/export_widget.dart b/auth/lib/ui/settings/data/export_widget.dart index ef438301c..0df748289 100644 --- a/auth/lib/ui/settings/data/export_widget.dart +++ b/auth/lib/ui/settings/data/export_widget.dart @@ -171,10 +171,12 @@ Future _exportCodes(BuildContext context, String fileContent) async { } Future _getAuthDataForExport() async { - final codes = await CodeStore.instance.getAllCodes(); + final allCodes = await CodeStore.instance.getAllCodes(); String data = ""; - for (final code in codes) { - data += "${code.rawData}\n"; + for (final code in allCodes) { + if (code.hasError) continue; + data += "${code.rawData.replaceAll(',', '%2C')}\n"; } + return data; } diff --git a/auth/lib/ui/settings/data/import/aegis_import.dart b/auth/lib/ui/settings/data/import/aegis_import.dart index b801e64a5..f6dd87252 100644 --- a/auth/lib/ui/settings/data/import/aegis_import.dart +++ b/auth/lib/ui/settings/data/import/aegis_import.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:convert/convert.dart'; +import 'package:convert/convert.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/code.dart'; import 'package:ente_auth/services/authenticator_service.dart'; @@ -150,7 +150,7 @@ Future _processAegisExportFile( } else { throw Exception('Invalid OTP type'); } - parsedCodes.add(Code.fromRawData(otpUrl)); + parsedCodes.add(Code.fromOTPAuthUrl(otpUrl)); } for (final code in parsedCodes) { diff --git a/auth/lib/ui/settings/data/import/bitwarden_import.dart b/auth/lib/ui/settings/data/import/bitwarden_import.dart index 7a562d82b..6878fa9f0 100644 --- a/auth/lib/ui/settings/data/import/bitwarden_import.dart +++ b/auth/lib/ui/settings/data/import/bitwarden_import.dart @@ -86,7 +86,7 @@ Future _processBitwardenExportFile( Code code; if (totp.contains("otpauth://")) { - code = Code.fromRawData(totp); + code = Code.fromOTPAuthUrl(totp); } else { var issuer = item['name']; var account = item['login']['username']; @@ -96,6 +96,7 @@ Future _processBitwardenExportFile( account, issuer, totp, + null, Code.defaultDigits, ); } diff --git a/auth/lib/ui/settings/data/import/encrypted_ente_import.dart b/auth/lib/ui/settings/data/import/encrypted_ente_import.dart index 511c9bbf9..3d7896f88 100644 --- a/auth/lib/ui/settings/data/import/encrypted_ente_import.dart +++ b/auth/lib/ui/settings/data/import/encrypted_ente_import.dart @@ -110,7 +110,7 @@ Future _decryptExportData( final parsedCodes = []; for (final code in splitCodes) { try { - parsedCodes.add(Code.fromRawData(code)); + parsedCodes.add(Code.fromOTPAuthUrl(code)); } catch (e) { Logger('EncryptedText').severe("Could not parse code", e); } diff --git a/auth/lib/ui/settings/data/import/google_auth_import.dart b/auth/lib/ui/settings/data/import/google_auth_import.dart index 12df41a14..c14752fa4 100644 --- a/auth/lib/ui/settings/data/import/google_auth_import.dart +++ b/auth/lib/ui/settings/data/import/google_auth_import.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; + import 'package:base32/base32.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/code.dart'; @@ -124,7 +125,7 @@ List parseGoogleAuth(String qrCodeData) { } else { throw Exception('Invalid OTP type'); } - codes.add(Code.fromRawData(otpUrl)); + codes.add(Code.fromOTPAuthUrl(otpUrl)); } return codes; } catch (e, s) { diff --git a/auth/lib/ui/settings/data/import/lastpass_import.dart b/auth/lib/ui/settings/data/import/lastpass_import.dart index 53f8b453d..8c36f0253 100644 --- a/auth/lib/ui/settings/data/import/lastpass_import.dart +++ b/auth/lib/ui/settings/data/import/lastpass_import.dart @@ -89,8 +89,8 @@ Future _processLastpassExportFile( // Build the OTP URL String otpUrl = - 'otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer'; - parsedCodes.add(Code.fromRawData(otpUrl)); + 'otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer'; + parsedCodes.add(Code.fromOTPAuthUrl(otpUrl)); } for (final code in parsedCodes) { diff --git a/auth/lib/ui/settings/data/import/plain_text_import.dart b/auth/lib/ui/settings/data/import/plain_text_import.dart index 03bc50dce..6867584b0 100644 --- a/auth/lib/ui/settings/data/import/plain_text_import.dart +++ b/auth/lib/ui/settings/data/import/plain_text_import.dart @@ -13,12 +13,15 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +final _logger = Logger('PlainText'); + class PlainTextImport extends StatelessWidget { const PlainTextImport({super.key}); @override Widget build(BuildContext context) { final l10n = context.l10n; + return Column( children: [ Text( @@ -101,20 +104,35 @@ Future _pickImportFile(BuildContext context) async { final progressDialog = createProgressDialog(context, l10n.pleaseWait); await progressDialog.show(); try { + final parsedCodes = []; File file = File(result.files.single.path!); final codes = await file.readAsString(); - List splitCodes = codes.split(","); - if (splitCodes.length == 1) { - splitCodes = const LineSplitter().convert(codes); - } - final parsedCodes = []; - for (final code in splitCodes) { - try { - parsedCodes.add(Code.fromRawData(code)); - } catch (e) { - Logger('PlainText').severe("Could not parse code", e); + + if (codes.startsWith('otpauth://')) { + List splitCodes = codes.split(","); + if (splitCodes.length == 1) { + splitCodes = const LineSplitter().convert(codes); + } + for (final code in splitCodes) { + try { + parsedCodes.add(Code.fromOTPAuthUrl(code)); + } catch (e) { + Logger('PlainText').severe("Could not parse code", e); + } + } + } else { + final decodedCodes = jsonDecode(codes); + List splitCodes = List.from(decodedCodes["items"]); + + for (final code in splitCodes) { + try { + parsedCodes.add(Code.fromExportJson(code)); + } catch (e) { + _logger.severe("Could not parse code", e); + } } } + for (final code in parsedCodes) { await CodeStore.instance.addCode(code, shouldSync: false); } diff --git a/auth/lib/ui/settings/data/import/raivo_plain_text_import.dart b/auth/lib/ui/settings/data/import/raivo_plain_text_import.dart index 48fc74888..3590a38b3 100644 --- a/auth/lib/ui/settings/data/import/raivo_plain_text_import.dart +++ b/auth/lib/ui/settings/data/import/raivo_plain_text_import.dart @@ -57,7 +57,7 @@ Future _pickRaivoJsonFile(BuildContext context) async { String path = result.files.single.path!; int? count = await _processRaivoExportFile(context, path); await progressDialog.hide(); - if(count != null) { + if (count != null) { await importSuccessDialog(context, count); } } catch (e) { @@ -70,9 +70,9 @@ Future _pickRaivoJsonFile(BuildContext context) async { } } -Future _processRaivoExportFile(BuildContext context,String path) async { +Future _processRaivoExportFile(BuildContext context, String path) async { File file = File(path); - if(path.endsWith('.zip')) { + if (path.endsWith('.zip')) { await showErrorDialog( context, context.l10n.sorry, @@ -105,7 +105,7 @@ Future _processRaivoExportFile(BuildContext context,String path) async { } else { throw Exception('Invalid OTP type'); } - parsedCodes.add(Code.fromRawData(otpUrl)); + parsedCodes.add(Code.fromOTPAuthUrl(otpUrl)); } for (final code in parsedCodes) { diff --git a/auth/lib/ui/settings/data/import/two_fas_import.dart b/auth/lib/ui/settings/data/import/two_fas_import.dart index ae5a05b0b..710d898d4 100644 --- a/auth/lib/ui/settings/data/import/two_fas_import.dart +++ b/auth/lib/ui/settings/data/import/two_fas_import.dart @@ -158,7 +158,7 @@ Future _process2FasExportFile( } else { throw Exception('Invalid OTP type'); } - parsedCodes.add(Code.fromRawData(otpUrl)); + parsedCodes.add(Code.fromOTPAuthUrl(otpUrl)); } for (final code in parsedCodes) { diff --git a/auth/lib/ui/settings_page.dart b/auth/lib/ui/settings_page.dart index 48fd6467c..0e99a1ea3 100644 --- a/auth/lib/ui/settings_page.dart +++ b/auth/lib/ui/settings_page.dart @@ -108,8 +108,9 @@ class SettingsPage extends StatelessWidget { await handleExportClick(context); } else { if (result.action == ButtonAction.second) { - bool hasCodes = - (await CodeStore.instance.getAllCodes()).isNotEmpty; + bool hasCodes = (await CodeStore.instance.getAllCodes()) + .where((element) => !element.hasError) + .isNotEmpty; if (hasCodes) { final hasAuthenticated = await LocalAuthenticationService .instance diff --git a/auth/lib/utils/email_util.dart b/auth/lib/utils/email_util.dart index 582449edb..8b0412228 100644 --- a/auth/lib/utils/email_util.dart +++ b/auth/lib/utils/email_util.dart @@ -146,7 +146,7 @@ Future getZippedLogsFile(BuildContext context) async { final encoder = ZipFileEncoder(); encoder.create(zipFilePath); await encoder.addDirectory(logsDirectory); - encoder.close(); + await encoder.close(); await dialog.hide(); return zipFilePath; } diff --git a/auth/linux/packaging/appimage/make_config.yaml b/auth/linux/packaging/appimage/make_config.yaml index 90db9c587..9a3004dcd 100644 --- a/auth/linux/packaging/appimage/make_config.yaml +++ b/auth/linux/packaging/appimage/make_config.yaml @@ -24,5 +24,6 @@ startup_notify: false # include: # - libcurl.so.4 include: - - libffi.so.7 + - libffi.so.8 - libtiff.so.5 + - libjpeg.so.8 diff --git a/auth/pubspec.lock b/auth/pubspec.lock index 772416042..a47858d53 100644 --- a/auth/pubspec.lock +++ b/auth/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: "direct main" description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: "0763b45fa9294197a2885c8567927e2830ade852e5c896fd4ab7e0e348d0f373" url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.5.0" args: dependency: transitive description: @@ -318,10 +318,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "639179e1cc0957779e10dd5b786ce180c477c4c0aca5aaba5d1700fa2e834801" + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" url: "https://pub.dev" source: hosted - version: "5.4.3" + version: "5.4.3+1" dotted_border: dependency: "direct main" description: @@ -468,10 +468,10 @@ packages: dependency: "direct main" description: name: flutter_email_sender - sha256: "5001e9158f91a8799140fb30a11ad89cd587244f30b4f848d87085985c49b60f" + sha256: fb515d4e073d238d0daf1d765e5318487b6396d46b96e0ae9745dbc9a133f97a url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.0.3" flutter_inappwebview: dependency: "direct main" description: @@ -565,10 +565,10 @@ packages: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" url: "https://pub.dev" source: hosted - version: "7.0.0+1" + version: "7.1.0" flutter_localizations: dependency: "direct main" description: flutter @@ -685,10 +685,10 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 + sha256: "81b68579e23fcbcada2db3d50302813d2371664afe6165bc78148050ab94bf66" url: "https://pub.dev" source: hosted - version: "8.2.4" + version: "8.2.5" freezed_annotation: dependency: transitive description: @@ -721,6 +721,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.6" + gradient_borders: + dependency: "direct main" + description: + name: gradient_borders + sha256: "69eeaff519d145a4c6c213ada1abae386bcc8981a4970d923e478ce7ba19e309" + url: "https://pub.dev" + source: hosted + version: "1.0.0" graphs: dependency: transitive description: @@ -813,18 +821,18 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "6.8.0" leak_tracker: dependency: transitive description: @@ -869,10 +877,10 @@ packages: dependency: "direct main" description: name: local_auth_android - sha256: "3bcd732dda7c75fcb7ddaef12e131230f53dcc8c00790d0d6efb3aa0fbbeda57" + sha256: e0e5b1ea247c5a0951c13a7ee13dc1beae69750e6a2e1910d1ed6a3cd4d56943 url: "https://pub.dev" source: hosted - version: "1.0.37" + version: "1.0.38" local_auth_darwin: dependency: "direct main" description: @@ -1133,10 +1141,10 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" + sha256: "79fbafed02cfdbe85ef3fd06c7f4bc2cbcba0177e61b765264853d4253b21744" url: "https://pub.dev" source: hosted - version: "3.8.0" + version: "3.9.0" pool: dependency: transitive description: @@ -1221,18 +1229,18 @@ packages: dependency: "direct main" description: name: sentry - sha256: fe99a06970b909a491b7f89d54c9b5119772e3a48a400308a6e129625b333f5b + sha256: e572d33a3ff1d69549f33ee828a8ff514047d43ca8eea4ab093d72461205aa3e url: "https://pub.dev" source: hosted - version: "7.19.0" + version: "7.20.1" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: fc013d4a753447320f62989b1871fdc1f20c77befcc8be3e38774dd7402e7a62 + sha256: ac8cf6bb849f3560353ae33672e17b2713809a4e8de0d3cf372e9e9c42013757 url: "https://pub.dev" source: hosted - version: "7.19.0" + version: "7.20.1" share_plus: dependency: "direct main" description: @@ -1419,10 +1427,10 @@ packages: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: d6c31c8511c441d1f12f20b607343df1afe4eddf24a1cf85021677c8eea26060 + sha256: fb2a106a2ea6042fe57de2c47074cc31539a941819c91e105b864744605da3f5 url: "https://pub.dev" source: hosted - version: "0.5.20" + version: "0.5.21" stack_trace: dependency: transitive description: @@ -1499,10 +1507,10 @@ packages: dependency: transitive description: name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5 url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.3" timing: dependency: transitive description: @@ -1595,10 +1603,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" url_launcher_windows: dependency: transitive description: @@ -1683,18 +1691,18 @@ packages: dependency: "direct main" description: name: win32 - sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.5.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" window_manager: dependency: "direct main" description: diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index b7a35b699..a8ea033d4 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: bip39: ^1.0.6 #done bloc: ^8.1.2 clipboard: ^0.1.3 - collection: # dart + collection: ^1.18.0 # dart confetti: ^0.7.0 connectivity_plus: ^5.0.2 convert: ^3.1.1 @@ -62,6 +62,7 @@ dependencies: flutter_svg: ^2.0.5 fluttertoast: ^8.1.1 google_nav_bar: ^5.0.5 #supported + gradient_borders: ^1.0.0 http: ^1.1.0 intl: ^0.18.0 json_annotation: ^4.5.0 @@ -129,6 +130,7 @@ flutter: - assets/simple-icons/_data/ - assets/custom-icons/icons/ - assets/custom-icons/_data/ + - assets/svg/ fonts: - family: Inter @@ -145,16 +147,38 @@ flutter: flutter_icons: android: "launcher_icon" adaptive_icon_foreground: "assets/generation-icons/icon-light-adaptive-fg.png" - adaptive_icon_background: "#ffffff" + adaptive_icon_background: "assets/generation-icons/icon-light-adaptive-bg.png" ios: true image_path: "assets/generation-icons/icon-light.png" remove_alpha_ios: true flutter_native_splash: - color: "#ffffff" + color: "#FFFFFF" color_dark: "#000000" - image: assets/splash-screen-light.png - image_dark: assets/splash-screen-dark.png - android_fullscreen: true + image: "assets/splash/splash-icon-fg.png" android_gravity: center ios_content_mode: center + android_12: + # The image parameter sets the splash screen icon image. If this parameter is not specified, + # the app's launcher icon will be used instead. + # Please note that the splash screen will be clipped to a circle on the center of the screen. + # App icon with an icon background: This should be 960×960 pixels, and fit within a circle + # 640 pixels in diameter. + # App icon without an icon background: This should be 1152×1152 pixels, and fit within a circle + # 768 pixels in diameter. + image: "assets/splash/splash-icon-fg-12.png" + + # Splash screen background color. + color: "#FFFFFF" + + # App icon background color. + #icon_background_color: "#111111" + + # The branding property allows you to specify an image used as branding in the splash screen. + #branding: assets/dart.png + + # The image_dark, color_dark, icon_background_color_dark, and branding_dark set values that + # apply when the device is in dark mode. If they are not specified, the app will use the + # parameters from above. + color_dark: "#000000" + #icon_background_color_dark: "#eeeeee" diff --git a/auth/test/models/code_test.dart b/auth/test/models/code_test.dart index 30ea23a4f..f51364118 100644 --- a/auth/test/models/code_test.dart +++ b/auth/test/models/code_test.dart @@ -1,9 +1,12 @@ +import 'dart:convert'; + import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/models/code_display.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { test("parseCodeFromRawData", () { - final code1 = Code.fromRawData( + final code1 = Code.fromOTPAuthUrl( "otpauth://totp/example%20finance%3Aee%40ff.gg?secret=ASKZNWOU6SVYAMVS", ); expect(code1.issuer, "example finance", reason: "issuerMismatch"); @@ -12,7 +15,7 @@ void main() { }); test("parseDocumentedFormat", () { - final code = Code.fromRawData( + final code = Code.fromOTPAuthUrl( "otpauth://totp/testdata@ente.io?secret=ASKZNWOU6SVYAMVS&issuer=GitHub", ); expect(code.issuer, "GitHub", reason: "issuerMismatch"); @@ -21,7 +24,7 @@ void main() { }); test("validateCount", () { - final code = Code.fromRawData( + final code = Code.fromOTPAuthUrl( "otpauth://hotp/testdata@ente.io?secret=ASKZNWOU6SVYAMVS&issuer=GitHub&counter=15", ); expect(code.issuer, "GitHub", reason: "issuerMismatch"); @@ -29,10 +32,29 @@ void main() { expect(code.secret, "ASKZNWOU6SVYAMVS"); expect(code.counter, 15); }); + + test("validateDisplay", () { + Code code = Code.fromOTPAuthUrl( + "otpauth://hotp/testdata@ente.io?secret=ASKZNWOU6SVYAMVS&issuer=GitHub&counter=15", + ); + expect(code.issuer, "GitHub", reason: "issuerMismatch"); + expect(code.account, "testdata@ente.io", reason: "accountMismatch"); + expect(code.secret, "ASKZNWOU6SVYAMVS"); + expect(code.counter, 15); + code = code.copyWith( + display: CodeDisplay(pinned: true, tags: ["tag1", "com,ma", ';;%\$']), + ); + final dataToStore = code.toOTPAuthUrlFormat(); + final restoredCode = Code.fromOTPAuthUrl(jsonDecode(dataToStore)); + expect(restoredCode.display.pinned, true); + expect(restoredCode.display.tags, ["tag1", "com,ma", ';;%\$']); + final secondDataToStore = restoredCode.toOTPAuthUrlFormat(); + expect(dataToStore, secondDataToStore); + }); // test("parseWithFunnyAccountName", () { - final code = Code.fromRawData( + final code = Code.fromOTPAuthUrl( "otpauth://totp/Mongo Atlas:Acc !@#444?algorithm=sha1&digits=6&issuer=Mongo Atlas&period=30&secret=NI4CTTFEV4G2JFE6", ); expect(code.issuer, "Mongo Atlas", reason: "issuerMismatch"); @@ -43,11 +65,11 @@ void main() { test("parseAndUpdateInChinese", () { const String rubberDuckQr = 'otpauth://totp/%E6%A9%A1%E7%9A%AE%E9%B8%AD?secret=2CWDCK4EOIN5DJDRMYUMYBBO4MKSR5AX&issuer=ente.io'; - final code = Code.fromRawData(rubberDuckQr); + final code = Code.fromOTPAuthUrl(rubberDuckQr); expect(code.account, '橡皮鸭'); final String updatedRawCode = code.copyWith(account: '伍迪', issuer: '鸭子').rawData; - final updateCode = Code.fromRawData(updatedRawCode); + final updateCode = Code.fromOTPAuthUrl(updatedRawCode); expect(updateCode.account, '伍迪', reason: 'updated accountMismatch'); expect(updateCode.issuer, '鸭子', reason: 'updated issuerMismatch'); }); diff --git a/auth/web/index.html b/auth/web/index.html index ef953df53..097159f9e 100644 --- a/auth/web/index.html +++ b/auth/web/index.html @@ -29,9 +29,92 @@ Auth - + - + + + + + + + + + + + + + + + @@ -40,6 +123,13 @@ + + + + + + + diff --git a/auth/web/splash/img/dark-1x.png b/auth/web/splash/img/dark-1x.png index 87f84c70e..91acb41ae 100644 Binary files a/auth/web/splash/img/dark-1x.png and b/auth/web/splash/img/dark-1x.png differ diff --git a/auth/web/splash/img/dark-2x.png b/auth/web/splash/img/dark-2x.png index ce01bec05..9a7c72afa 100644 Binary files a/auth/web/splash/img/dark-2x.png and b/auth/web/splash/img/dark-2x.png differ diff --git a/auth/web/splash/img/dark-3x.png b/auth/web/splash/img/dark-3x.png index 75f4b1f3c..5b4d99582 100644 Binary files a/auth/web/splash/img/dark-3x.png and b/auth/web/splash/img/dark-3x.png differ diff --git a/auth/web/splash/img/dark-4x.png b/auth/web/splash/img/dark-4x.png index 2beb1c816..1666311d2 100644 Binary files a/auth/web/splash/img/dark-4x.png and b/auth/web/splash/img/dark-4x.png differ diff --git a/auth/web/splash/img/light-1x.png b/auth/web/splash/img/light-1x.png index 899cecf22..91acb41ae 100644 Binary files a/auth/web/splash/img/light-1x.png and b/auth/web/splash/img/light-1x.png differ diff --git a/auth/web/splash/img/light-2x.png b/auth/web/splash/img/light-2x.png index 4bb7a5751..9a7c72afa 100644 Binary files a/auth/web/splash/img/light-2x.png and b/auth/web/splash/img/light-2x.png differ diff --git a/auth/web/splash/img/light-3x.png b/auth/web/splash/img/light-3x.png index 176f0c723..5b4d99582 100644 Binary files a/auth/web/splash/img/light-3x.png and b/auth/web/splash/img/light-3x.png differ diff --git a/auth/web/splash/img/light-4x.png b/auth/web/splash/img/light-4x.png index a0d1a26f7..1666311d2 100644 Binary files a/auth/web/splash/img/light-4x.png and b/auth/web/splash/img/light-4x.png differ