diff --git a/.circleci/config.yml b/.circleci/config.yml index d9867073..42c583e1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,7 +9,7 @@ jobs: steps: - checkout - restore_cache: - key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} + key: jars-{{ checksum "build.gradle.kts" }}-{{ checksum "app/build.gradle.kts" }} # - run: # name: Chmod permissions #if permission for Gradlew Dependencies fail, use this. # command: sudo chmod +x ./gradlew @@ -22,7 +22,7 @@ jobs: - save_cache: paths: - ~/.gradle - key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} + key: jars-{{ checksum "build.gradle.kts" }}-{{ checksum "app/build.gradle.kts" }} - run: name: Run Tests command: ./gradlew lint test diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index d44d3f25..00000000 --- a/app/build.gradle +++ /dev/null @@ -1,212 +0,0 @@ -apply plugin: "com.android.application" -apply plugin: "kotlin-android" -apply plugin: "org.jetbrains.kotlin.android" - -android { - compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 - } - - signingConfigs { - debug { - def props = new Properties() - def localPropsFile = rootProject.file('local.properties') - if (localPropsFile.exists()) { - localPropsFile.withInputStream { props.load(it) } - } - - storeFile = file(props['storeFile'] ?: "") - storePassword = props['storePassword'] ?: "" - keyAlias = props['keyAlias'] ?: "" - keyPassword = props['keyPassword'] ?: "" - } - } - - compileSdk 34 - defaultConfig { - applicationId "net.opendasharchive.openarchive" - minSdkVersion 29 - //noinspection OldTargetApi - targetSdkVersion 34 - versionCode 30005 - versionName "0.7.7" - archivesBaseName = "Save-$versionName" - multiDexEnabled true - vectorDrawables.useSupportLibrary = true - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - flavorDimensions += "free" - buildTypes { - release { - signingConfig signingConfigs.debug - minifyEnabled false - shrinkResources false - proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" - } - - debug { - signingConfig signingConfigs.debug - } - } - packagingOptions { - resources { - excludes += ["META-INF/LICENSE.txt", "META-INF/NOTICE.txt", "META-INF/LICENSE", "META-INF/NOTICE", "META-INF/DEPENDENCIES", "LICENSE.txt"] - } - } - productFlavors { - releaseflavor { - dimension "free" - applicationId "net.opendasharchive.openarchive.release" - } - } - - kotlinOptions { - jvmTarget = "1.8" - } - - buildFeatures { - viewBinding true - buildConfig true - compose true - } - - lint { - abortOnError false - } - - composeOptions { - kotlinCompilerExtensionVersion "1.5.13" - } - - namespace "net.opendasharchive.openarchive" -} - -dependencies { - - implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.23" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0" - -// implementation "androidx.core:core-ktx:1.13.1" - implementation "androidx.recyclerview:recyclerview:1.3.2" - implementation "androidx.appcompat:appcompat:1.6.1" - implementation "androidx.biometric:biometric:1.1.0" - implementation "androidx.constraintlayout:constraintlayout:2.1.4" - implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" - implementation "androidx.legacy:legacy-support-v4:1.0.0" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0" - implementation "androidx.preference:preference-ktx:1.2.1" - implementation "androidx.work:work-runtime:2.9.0" - implementation "androidx.work:work-runtime-ktx:2.9.0" - implementation "androidx.work:work-testing:2.9.0" - - implementation "androidx.compose.ui:ui:1.6.7" - implementation "androidx.compose.material3:material3:1.2.1" - implementation "androidx.compose.foundation:foundation:1.6.7" - implementation "androidx.compose.ui:ui-tooling-preview:1.6.7" - implementation "androidx.activity:activity-compose:1.9.0" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0" - implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0" - - implementation "io.insert-koin:koin-core:3.5.3" - implementation "io.insert-koin:koin-android:3.5.3" - implementation "io.insert-koin:koin-androidx-compose:3.5.3" - - implementation "com.github.satyan:sugar:1.5" - - implementation "com.google.code.gson:gson:2.10.1" - implementation "com.squareup.okhttp3:okhttp:4.12.0" - - // adding web dav support: https://github.com/thegrizzlylabs/sardine-android' - implementation "com.github.guardianproject:sardine-android:89f7eae512" - - implementation "com.google.android.material:material:1.11.0" - implementation "androidx.compose.material:material-icons-extended:1.6.7" - - implementation "com.github.bumptech.glide:glide:4.16.0" - annotationProcessor "com.github.bumptech.glide:compiler:4.16.0" - implementation "com.github.derlio:audio-waveform:v1.0.1" - implementation "com.github.esafirm:android-image-picker:3.0.0" - implementation "com.facebook.fresco:fresco:3.5.0" - implementation "com.squareup.picasso:picasso:2.5.2" - - implementation "com.github.abdularis:circularimageview:1.4" - - implementation "org.cleaninsights.sdk:clean-insights-sdk:2.8.0" - implementation "info.guardianproject.netcipher:netcipher:2.2.0-alpha" - - //from here: https://github.com/guardianproject/proofmode - implementation("org.proofmode:android-libproofmode:1.0.27") { - - transitive = false - - exclude group: "org.bitcoinj" - exclude group: "com.google.protobuf" - exclude group: "org.slf4j" - exclude group: "net.jcip" - exclude group: "commons-cli" - exclude group: "org.json" - exclude group: "com.google.guava" - exclude group: "com.google.guava", module: "guava-jdk5" - exclude group: "com.google.code.findbugs", module: "annotations" - exclude group: "com.squareup.okio", module: "okio" - } - - implementation "com.google.guava:guava:31.0.1-jre" - implementation "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" - - implementation "org.bouncycastle:bcpkix-jdk15to18:1.72" - implementation "org.bouncycastle:bcprov-jdk15to18:1.72" - api "org.bouncycastle:bcpg-jdk15to18:1.71" - - implementation "com.tbuonomo:dotsindicator:5.0" - implementation "com.guolindev.permissionx:permissionx:1.6.4" - - implementation "com.jakewharton.timber:timber:5.0.1" - - // Google Drive API - implementation "com.google.android.gms:play-services-auth:21.1.1" - implementation "com.google.http-client:google-http-client-gson:1.42.1" - implementation "com.google.api-client:google-api-client-android:1.26.0" - implementation "com.google.apis:google-api-services-drive:v3-rev136-1.25.0" - - // Tor - implementation "info.guardianproject:tor-android:0.4.7.14" - implementation "info.guardianproject:jtorctl:0.4.5.7" - - // New Play libraries - implementation "com.google.android.play:asset-delivery:2.2.2" - implementation "com.google.android.play:asset-delivery-ktx:2.2.2" - - implementation "com.google.android.play:feature-delivery:2.1.0" - implementation "com.google.android.play:feature-delivery-ktx:2.1.0" - - implementation "com.google.android.play:review:2.0.1" - implementation "com.google.android.play:review-ktx:2.0.1" - - implementation "com.google.android.play:app-update:2.1.0" - implementation "com.google.android.play:app-update-ktx:2.1.0" - - // Tests - testImplementation "junit:junit:4.13.2" - testImplementation "org.robolectric:robolectric:4.7.3" - androidTestImplementation "androidx.test.ext:junit:1.1.5" - androidTestImplementation "androidx.test:runner:1.5.2" - - // Pretty Logging - implementation "com.orhanobut:logger:2.2.0" -} - -configurations { - all*.exclude group: "com.google.guava", module: "listenablefuture" -} - -/** - testdroid {username '$bbusername' - password '$bbpassword' - deviceGroup 'gpdevices' - mode "FULL_RUN" - projectName "OASave"}**/ - diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..eb97e32c --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,262 @@ +import java.util.Properties + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") + id("org.jetbrains.kotlin.plugin.serialization") + id("com.google.devtools.ksp") +} +android { + + compileSdk = 34 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + defaultConfig { + applicationId = "net.opendasharchive.openarchive" + minSdk = 29 + targetSdk = 34 + versionCode = 30006 + versionName = "0.7.8" + multiDexEnabled = true + vectorDrawables.useSupportLibrary = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + base { + archivesName.set("save-${project.version}") + } + + buildFeatures { + viewBinding = true + buildConfig = true + compose = true + } + + buildTypes { + + getByName("release") { + signingConfig = signingConfigs.getByName("debug") + isMinifyEnabled = false + isShrinkResources = false + applicationIdSuffix = ".release" + proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + } + + getByName("debug") { + signingConfig = signingConfigs.getByName("debug") + applicationIdSuffix = ".debug" + isMinifyEnabled = false + } + } + + signingConfigs { + getByName("debug") { + val props = Properties() + val localPropsFile = rootProject.file("local.properties") + if (localPropsFile.exists()) { + localPropsFile.inputStream().use { props.load(it) } + } + + storeFile = file(props["storeFile"] as? String ?: "") + storePassword = props["storePassword"] as? String ?: "" + keyAlias = props["keyAlias"] as? String ?: "" + keyPassword = props["keyPassword"] as? String ?: "" + } + } + + packaging { + resources { + excludes.addAll( + listOf( + "META-INF/LICENSE.txt", "META-INF/NOTICE.txt", "META-INF/LICENSE", + "META-INF/NOTICE", "META-INF/DEPENDENCIES", "LICENSE.txt" + ) + ) + } + } + + lint { + abortOnError = false + } + + namespace = "net.opendasharchive.openarchive" + + configurations.all { + resolutionStrategy { + force("org.bouncycastle:bcprov-jdk15to18:1.72") + exclude(group = "org.bouncycastle", module = "bcprov-jdk15on") + } + } +} + +dependencies { + + // Core Kotlin and Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") + + + // AndroidX Libraries + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.recyclerview:recyclerview:1.3.2") + implementation("androidx.recyclerview:recyclerview-selection:1.1.0") + implementation("androidx.constraintlayout:constraintlayout:2.2.0") + implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0") + implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0") + implementation("androidx.core:core-splashscreen:1.0.1") + + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.7") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7") + implementation("androidx.navigation:navigation-fragment-ktx:2.8.5") + implementation("androidx.navigation:navigation-ui-ktx:2.8.5") + + implementation("androidx.preference:preference-ktx:1.2.1") + implementation("androidx.biometric:biometric:1.1.0") + implementation("androidx.work:work-runtime-ktx:2.9.1") + implementation("androidx.security:security-crypto-ktx:1.1.0-alpha06") + + // Compose Preferences + implementation("me.zhanghai.compose.preference:library:1.1.1") + + // Material Design + implementation("com.google.android.material:material:1.12.0") + + // AndroidX SwipeRefreshLayout + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") + + // Compose Libraries + implementation("androidx.compose.ui:ui:1.7.6") + implementation("androidx.compose.foundation:foundation:1.7.6") + implementation("androidx.compose.material3:material3:1.3.1") + implementation("androidx.compose.ui:ui-tooling-preview:1.7.6") + implementation("androidx.activity:activity-compose:1.9.3") + implementation("androidx.compose.material:material-icons-extended:1.7.6") + + // Navigation + implementation("androidx.navigation:navigation-compose:2.8.5") + + // Preference + implementation("androidx.preference:preference-ktx:1.2.1") + + // Dependency Injection + implementation("io.insert-koin:koin-core:4.1.0-Beta5") + implementation("io.insert-koin:koin-android:4.1.0-Beta5") + implementation("io.insert-koin:koin-androidx-compose:4.1.0-Beta5") + + // Image Libraries + implementation("com.github.bumptech.glide:glide:4.16.0") + annotationProcessor("com.github.bumptech.glide:compiler:4.16.0") + implementation("com.github.esafirm:android-image-picker:3.0.0") + implementation("com.squareup.picasso:picasso:2.5.2") + implementation("io.coil-kt:coil-compose:2.7.0") + implementation("io.coil-kt:coil-video:2.7.0") + + // Networking and Data + // Networking + implementation("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.squareup.retrofit2:converter-gson:2.11.0") + implementation("com.google.code.gson:gson:2.11.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + implementation("com.github.guardianproject:sardine-android:89f7eae512") + + // Utility Libraries + implementation("com.jakewharton.timber:timber:5.0.1") + implementation("com.orhanobut:logger:2.2.0") + implementation("com.github.abdularis:circularimageview:1.4") + implementation("com.tbuonomo:dotsindicator:5.1.0") + implementation("com.guolindev.permissionx:permissionx:1.6.4") + + // Barcode Scanning + implementation("com.google.zxing:core:3.4.1") + implementation("com.journeyapps:zxing-android-embedded:4.2.0") + + // Security and Encryption + implementation("org.bouncycastle:bcpkix-jdk15to18:1.72") + implementation("org.bouncycastle:bcprov-jdk15to18:1.72") + api("org.bouncycastle:bcpg-jdk15to18:1.71") + + // Google Play Services + implementation("com.google.android.gms:play-services-auth:21.3.0") +// implementation("com.google.android.play:core-ktx:1.8.1") +// implementation("com.google.android.play:asset-delivery-ktx:2.3.0") +// implementation("com.google.android.play:feature-delivery-ktx:2.1.0") +// implementation("com.google.android.play:review-ktx:2.0.2") +// implementation("com.google.android.play:app-update-ktx:2.1.0") + + // Google Drive API + implementation("com.google.http-client:google-http-client-gson:1.42.3") + implementation("com.google.api-client:google-api-client-android:1.26.0") + implementation("com.google.apis:google-api-services-drive:v3-rev136-1.25.0") + + // Tor Libraries + implementation("info.guardianproject:tor-android:0.4.7.14") + implementation("info.guardianproject:jtorctl:0.4.5.7") + + implementation("org.bitcoinj:bitcoinj-core:0.16.2") + implementation("com.eclipsesource.j2v8:j2v8:6.2.1@aar") + + // ProofMode //from here: https://github.com/guardianproject/proofmode + implementation("org.proofmode:android-libproofmode:1.0.26") { + //transitive = false + exclude(group = "org.bitcoinj") + exclude(group = "com.google.protobuf") + exclude(group = "org.slf4j") + exclude(group = "net.jcip") + exclude(group = "commons-cli") + exclude(group = "org.json") + exclude(group = "com.google.guava") + exclude(group = "com.google.guava", module = "guava-jdk5") + exclude(group = "com.google.code.findbugs", module = "annotations") + exclude(group = "com.squareup.okio", module = "okio") + } + + // Guava Conflicts + implementation("com.google.guava:guava:31.0.1-jre") + implementation("com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava") + + + implementation("com.github.satyan:sugar:1.5") + + + // adding web dav support: https://github.com/thegrizzlylabs/sardine-android' + implementation("com.github.guardianproject:sardine-android:89f7eae512") + + + implementation("com.github.derlio:audio-waveform:v1.0.1") + + + implementation("org.cleaninsights.sdk:clean-insights-sdk:2.8.0") + implementation("info.guardianproject.netcipher:netcipher:2.2.0-alpha") + + + // Tests + testImplementation("junit:junit:4.13.2") + testImplementation("org.robolectric:robolectric:4.10.3") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test:runner:1.6.2") + testImplementation("androidx.work:work-testing:2.9.1") + debugImplementation("androidx.compose.ui:ui-tooling:1.7.6") +} + +configurations.all { + exclude(group = "com.google.guava", module = "listenablefuture") +} + +/** +testdroid {username '$bbusername' +password '$bbpassword' +deviceGroup 'gpdevices' +mode "FULL_RUN" +projectName "OASave"}**/ + diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 12125fc7..a6e7fc29 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -2,7 +2,7 @@ # By default, the flags in this file are appended to flags specified # in /home/josh/android-sdks/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. +# directive in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4d250a93..dbdbb8d1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -52,7 +52,7 @@ android:networkSecurityConfig="@xml/network_security_config" android:requestLegacyExternalStorage="true" android:supportsRtl="true" - android:theme="@style/AppTheme.NoActionBar" + android:theme="@style/Theme.SaveApp.Starting" tools:ignore="UnusedAttribute,LockedOrientationActivity" tools:replace="android:icon,android:allowBackup"> private val c23_teal_10 = Color(0xff001b19) // v=10.6 --> private val c23_powder_blue = Color(0xffaae6e1) +private val inputBackgroundLight = Color(0xfffffbf0) +private val inputBackgroundDark = Color(0xff212021) +private val darkPrimary = Color(0xff000A0A) + @Immutable data class ColorTheme( val material: ColorScheme, @@ -36,7 +43,8 @@ data class ColorTheme( val onDisabledContainer: Color = c23_light_grey, ) -private val LightColorScheme = ColorTheme( +@Composable +internal fun lightColorScheme() = ColorTheme( material = lightColorScheme( primary = c23_teal, @@ -59,8 +67,8 @@ private val LightColorScheme = ColorTheme( errorContainer = Color.Red, onErrorContainer = Color.Black, - background = Color.White, - onBackground = Color.Black, + background = colorResource(R.color.colorBackground), + onBackground = colorResource(R.color.colorOnBackground), surface = c23_light_grey, onSurface = Color.Black, @@ -76,13 +84,15 @@ private val LightColorScheme = ColorTheme( scrim = c23_light_grey, surfaceBright = c23_light_grey, surfaceContainer = Color.White, + surfaceContainerHighest = inputBackgroundLight, surfaceDim = c23_light_grey ), ) -private val DarkColorScheme = ColorTheme( +@Composable +internal fun darkColorScheme() = ColorTheme( material = darkColorScheme( - primary = c23_teal, + primary = darkPrimary, onPrimary = Color.Black, primaryContainer = c23_teal, onPrimaryContainer = Color.White, @@ -102,8 +112,8 @@ private val DarkColorScheme = ColorTheme( errorContainer = Color.Red, onErrorContainer = Color.Black, - background = Color.Black, - onBackground = Color.White, + background = colorResource(R.color.colorBackground), + onBackground = colorResource(R.color.colorOnBackground), surface = c23_darker_grey, onSurface = Color.White, @@ -119,11 +129,11 @@ private val DarkColorScheme = ColorTheme( scrim = c23_light_grey, surfaceBright = c23_grey, surfaceContainer = c23_medium_grey, + surfaceContainerHighest = inputBackgroundDark, surfaceDim = c23_dark_grey ), ) -fun getThemeColors(isDarkTheme: Boolean) = if (isDarkTheme) DarkColorScheme else LightColorScheme - -val LocalColors = staticCompositionLocalOf { LightColorScheme } - +val LocalColors = staticCompositionLocalOf { + error("LocalColors not provided. Wrap your composables in the Theme function.") +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Shape.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Shape.kt new file mode 100644 index 00000000..a8cbe804 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Shape.kt @@ -0,0 +1,11 @@ +package net.opendasharchive.openarchive.core.presentation.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt index e0bfbea0..44a74886 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt @@ -13,7 +13,7 @@ fun Theme( ) { val isDarkTheme by rememberUpdatedState(newValue = isSystemInDarkTheme()) - val colors = getThemeColors(isDarkTheme) + val colors = if (isDarkTheme) darkColorScheme() else lightColorScheme() val dimensions = getThemeDimensions(isDarkTheme) @@ -23,7 +23,8 @@ fun Theme( ) { MaterialTheme( colorScheme = colors.material, - content = content + content = content, + shapes = Shapes ) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt index cafc804b..06d1a955 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt @@ -3,6 +3,8 @@ package net.opendasharchive.openarchive.features.core import android.view.MotionEvent import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.appbar.MaterialToolbar +import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.util.Prefs abstract class BaseActivity : AppCompatActivity() { @@ -39,4 +41,25 @@ abstract class BaseActivity : AppCompatActivity() { window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } } + + fun setupToolbar( + title: String = "", + subtitle: String? = null, + showBackButton: Boolean = true + ) { + val toolbar: MaterialToolbar = findViewById(R.id.common_toolbar) + setSupportActionBar(toolbar) + supportActionBar?.title = title + + if (subtitle != null) { + supportActionBar?.subtitle = subtitle + } + + if (showBackButton) { + supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + } else { + supportActionBar?.setDisplayHomeAsUpEnabled(false) + } + } } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderActivity.kt index eb23d402..12dd7285 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderActivity.kt @@ -5,16 +5,12 @@ import android.os.Bundle import android.view.MenuItem import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.databinding.ActivityAddFolderBinding import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity -import net.opendasharchive.openarchive.util.extensions.Position import net.opendasharchive.openarchive.util.extensions.hide -import net.opendasharchive.openarchive.util.extensions.setDrawable -import net.opendasharchive.openarchive.util.extensions.tint class AddFolderActivity : BaseActivity() { @@ -29,48 +25,44 @@ class AddFolderActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - setResult(RESULT_OK, it.data) - finish() - } - else { - val name = it.data?.getStringExtra(EXTRA_FOLDER_NAME) + mResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + setResult(RESULT_OK, it.data) + finish() + } else { + val name = it.data?.getStringExtra(EXTRA_FOLDER_NAME) - if (!name.isNullOrBlank()) { - val i = Intent(this, CreateNewFolderActivity::class.java) - i.putExtra(EXTRA_FOLDER_NAME, name) + if (!name.isNullOrBlank()) { + val i = Intent(this, CreateNewFolderActivity::class.java) + i.putExtra(EXTRA_FOLDER_NAME, name) - mResultLauncher.launch(i) + mResultLauncher.launch(i) + } } } - } mBinding = ActivityAddFolderBinding.inflate(layoutInflater) setContentView(mBinding.root) - mBinding.newFolder.setOnClickListener { + setupToolbar( + title = getString(R.string.add_a_folder), + showBackButton = true + ) + + mBinding.addFolderContainer.setOnClickListener { setFolder(false) } - mBinding.browseFolders.setOnClickListener { + mBinding.browseFolderContainer.setOnClickListener { setFolder(true) } - val arrow = ContextCompat.getDrawable(this, R.drawable.ic_arrow_right) - arrow?.tint(ContextCompat.getColor(this, R.color.colorPrimary)) - - mBinding.newFolderText.setDrawable(arrow, Position.End, tint = false) - mBinding.browseFoldersText.setDrawable(arrow, Position.End, tint = false) - - setSupportActionBar(mBinding.toolbar) - supportActionBar?.title = "" - supportActionBar?.setDisplayHomeAsUpEnabled(true) // We cannot browse the Internet Archive. Directly forward to creating a project, // as it doesn't make sense to show a one-option menu. if (Space.current?.tType == Space.Type.INTERNET_ARCHIVE) { - mBinding.browseFolders.hide() + mBinding.browseFolderContainer.hide() finish() setFolder(false) @@ -97,7 +89,11 @@ class AddFolderActivity : BaseActivity() { return } - mResultLauncher.launch(Intent(this, - if (browse) BrowseFoldersActivity::class.java else CreateNewFolderActivity::class.java)) + mResultLauncher.launch( + Intent( + this, + if (browse) BrowseFoldersActivity::class.java else CreateNewFolderActivity::class.java + ) + ) } } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersActivity.kt index 3de2283a..a04be3d3 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersActivity.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem +import androidx.activity.viewModels import androidx.recyclerview.widget.LinearLayoutManager import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.databinding.ActivityBrowseFoldersBinding @@ -17,9 +18,9 @@ import java.util.Date class BrowseFoldersActivity : BaseActivity() { private lateinit var mBinding: ActivityBrowseFoldersBinding - private lateinit var mViewModel: BrowseFoldersViewModel + private val mViewModel: BrowseFoldersViewModel by viewModels() - private var mSelected: BrowseFoldersViewModel.Folder? = null + private var mSelected: Folder? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -27,12 +28,10 @@ class BrowseFoldersActivity : BaseActivity() { mBinding = ActivityBrowseFoldersBinding.inflate(layoutInflater) setContentView(mBinding.root) - mViewModel = BrowseFoldersViewModel() - - setSupportActionBar(mBinding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - title = getString(R.string.browse_existing) + setupToolbar( + title = getString(R.string.browse_existing), + showBackButton = true + ) mBinding.rvFolderList.layoutManager = LinearLayoutManager(this) @@ -42,8 +41,9 @@ class BrowseFoldersActivity : BaseActivity() { mViewModel.folders.observe(this) { mBinding.projectsEmpty.toggle(it.isEmpty()) - mBinding.rvFolderList.adapter = BrowseFoldersAdapter(it) { name -> - mSelected = name + mBinding.rvFolderList.adapter = BrowseFoldersAdapter(it) { folder -> + this.mSelected = folder + invalidateOptionsMenu() } } @@ -58,16 +58,16 @@ class BrowseFoldersActivity : BaseActivity() { return super.onCreateOptionsMenu(menu) } + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + val addMenuItem = menu?.findItem(R.id.action_add) + addMenuItem?.isVisible = mSelected != null + return super.onPrepareOptionsMenu(menu) + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - android.R.id.home -> { - finish() - - return true - } R.id.action_add -> { addFolder(mSelected) - return true } } @@ -75,7 +75,7 @@ class BrowseFoldersActivity : BaseActivity() { return super.onOptionsItemSelected(item) } - private fun addFolder(folder: BrowseFoldersViewModel.Folder?) { + private fun addFolder(folder: Folder?) { if (folder == null) return val space = Space.current ?: return diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt index 580859f7..cc86d19b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt @@ -1,69 +1,68 @@ package net.opendasharchive.openarchive.features.folders -import android.graphics.drawable.Drawable import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.databinding.FolderRowBinding -import net.opendasharchive.openarchive.util.extensions.clone -import net.opendasharchive.openarchive.util.extensions.scaled -import net.opendasharchive.openarchive.util.extensions.tint import java.text.SimpleDateFormat class BrowseFoldersAdapter( - private val folders: List = emptyList(), - private val onClick: (folder: BrowseFoldersViewModel.Folder) -> Unit + private val folders: List = emptyList(), + private val onClick: (folder: Folder) -> Unit ) : RecyclerView.Adapter() { companion object { - private var sOriginalColor = 0 - private var sHighlightColor = 0 - private var sIcon: Drawable? = null private val formatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.LONG, SimpleDateFormat.MEDIUM) } - private var mSelected = -1 + private var mSelected: Folder? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder { val binding = FolderRowBinding.inflate(LayoutInflater.from(parent.context), parent, false) val context = binding.root.context - if (sOriginalColor == 0) sOriginalColor = binding.name.currentTextColor - if (sHighlightColor == 0) sHighlightColor = ContextCompat.getColor(context, R.color.colorPrimary) - if (sIcon == null) sIcon = ContextCompat.getDrawable(context, R.drawable.ic_folder)?.scaled(0.75, context) - return FolderViewHolder(binding, onClick) } override fun onBindViewHolder(holder: FolderViewHolder, position: Int) { - holder.onBindView(position, mSelected == position) + holder.bind(folders[position]) } - override fun getItemCount(): Int { - return folders.size - } + override fun getItemCount(): Int = folders.size + + inner class FolderViewHolder(private val binding: FolderRowBinding, private val onClick: (folder: Folder) -> Unit) : RecyclerView.ViewHolder(binding.root) { + + fun bind(folder: Folder) { + + val isSelected = mSelected == folder + + itemView.isSelected = isSelected - inner class FolderViewHolder( - private val binding: FolderRowBinding, - private val onClick: (folder: BrowseFoldersViewModel.Folder) -> Unit - ) : RecyclerView.ViewHolder(binding.root) - { - fun onBindView(position: Int, selected: Boolean) { - val color = if (selected) sHighlightColor else sOriginalColor - binding.icon.setImageDrawable(sIcon?.clone()?.tint(color)) - binding.name.setTextColor(color) + val folderIconRes = if (isSelected) R.drawable.ic_folder_selected else R.drawable.ic_folder_unselected - binding.name.text = folders[position].name - binding.timestamp.text = formatter.format(folders[position].modified) + binding.icon.setImageDrawable(ContextCompat.getDrawable(binding.icon.context, folderIconRes)) + + + binding.name.text = folder.name + binding.timestamp.text = formatter.format(folder.modified) + + binding.rvTick.visibility = if (isSelected) View.VISIBLE else View.INVISIBLE binding.root.setOnClickListener { - mSelected = position - this@BrowseFoldersAdapter.notifyDataSetChanged() + if (mSelected == folder) return@setOnClickListener + + val previousSelected = mSelected + mSelected = folder + + // Notify changes for previous and current selection + notifyItemChanged(folders.indexOf(previousSelected)) + notifyItemChanged(folders.indexOf(mSelected)) - onClick.invoke(folders[position]) + onClick.invoke(folder) } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt index 1ec74006..3eb5945b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt @@ -15,9 +15,9 @@ import timber.log.Timber import java.io.IOException import java.util.Date -class BrowseFoldersViewModel : ViewModel() { +data class Folder(val name: String, val modified: Date) - data class Folder(val name: String, val modified: Date) +class BrowseFoldersViewModel : ViewModel() { private val mFolders = MutableLiveData>() diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderActivity.kt index bf00aa97..ea764496 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderActivity.kt @@ -29,9 +29,10 @@ class CreateNewFolderActivity : BaseActivity() { mBinding = ActivityCreateNewFolderBinding.inflate(layoutInflater) setContentView(mBinding.root) - setSupportActionBar(mBinding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.title = getString(R.string.new_folder) + setupToolbar( + title = "Create Folder", + showBackButton = true + ) mBinding.newFolder.setText(intent.getStringExtra(AddFolderActivity.EXTRA_FOLDER_NAME)) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt index be5500f7..c0ba91eb 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt @@ -1,14 +1,19 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation -import android.app.Activity import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity -import net.opendasharchive.openarchive.CleanInsightsManager +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.ui.Modifier +import net.opendasharchive.openarchive.core.presentation.theme.Theme import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ComposeAppBar import net.opendasharchive.openarchive.features.main.MainActivity @Deprecated("use jetpack compose") @@ -19,9 +24,27 @@ class InternetArchiveActivity : AppCompatActivity() { val (space, isNewSpace) = intent.extras.getSpace(Space.Type.INTERNET_ARCHIVE) setContent { - InternetArchiveScreen(space, isNewSpace) { - finish(it) + + Theme { + Scaffold( + topBar = { + ComposeAppBar( + title = if (isNewSpace) "Add Internet Archive" else "Edit Internet Archive", + onNavigationAction = { finish(IAResult.Cancelled) } + ) + } + ) { paddingValues -> + Box(modifier = Modifier + .fillMaxSize() + .padding(paddingValues)) { + InternetArchiveScreen(space, isNewSpace) { + finish(it) + } + } + } } + + } } @@ -33,7 +56,7 @@ class InternetArchiveActivity : AppCompatActivity() { } IAResult.Deleted -> Space.navigate(this) - else -> Unit + IAResult.Cancelled -> onBackPressed() } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt index 6c934e1f..5aeff9b9 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt @@ -6,16 +6,17 @@ import android.view.View import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult import net.opendasharchive.openarchive.features.internetarchive.presentation.components.bundleWithNewSpace import net.opendasharchive.openarchive.features.internetarchive.presentation.components.bundleWithSpaceId import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace +import net.opendasharchive.openarchive.features.onboarding.BaseFragment +import net.opendasharchive.openarchive.features.onboarding.ToolbarConfigurable @Deprecated("only used for backward compatibility") -class InternetArchiveFragment : Fragment() { +class InternetArchiveFragment : BaseFragment(), ToolbarConfigurable { override fun onCreateView( inflater: LayoutInflater, @@ -58,4 +59,7 @@ class InternetArchiveFragment : Fragment() { @JvmStatic fun newInstance() = newInstance(args = bundleWithNewSpace()) } + + override fun getToolbarTitle() = "Internet Archive" + override fun shouldShowBackButton() = true } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt index 2f6d1a07..d1cf343c 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt @@ -18,12 +18,12 @@ import androidx.compose.ui.graphics.ColorFilter.Companion.tint import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.presentation.theme.Theme import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions @@ -51,16 +51,10 @@ fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 1 contentDescription = stringResource( id = R.string.internet_archive ), - colorFilter = tint(colorResource(id = R.color.colorPrimary)) + colorFilter = tint(colorResource(id = R.color.colorTertiary)) ) } Column(modifier = Modifier.padding(start = ThemeDimensions.spacing.medium)) { - Text( - text = stringResource(id = R.string.internet_archive), - fontSize = titleSize, - fontWeight = FontWeight.Bold, - color = ThemeColors.material.onSurface - ) Text( text = stringResource(id = R.string.internet_archive_description), color = ThemeColors.material.onSurfaceVariant @@ -71,6 +65,9 @@ fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 1 @Composable @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) private fun InternetArchiveHeaderPreview() { - InternetArchiveHeader() + Theme { + InternetArchiveHeader() + } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt index c851cde5..d1b434c3 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt @@ -10,9 +10,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -22,10 +24,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.presentation.theme.Theme import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions import net.opendasharchive.openarchive.core.state.Dispatch @@ -33,6 +37,8 @@ import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader import net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsViewModel.Action +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.CustomTextField +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.DefaultScaffoldPreview import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -57,6 +63,7 @@ fun InternetArchiveDetailsScreen(space: Space, onResult: (IAResult) -> Unit) { InternetArchiveDetailsContent(state, viewModel::dispatch) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun InternetArchiveDetailsContent( state: InternetArchiveDetailsState, @@ -65,6 +72,7 @@ private fun InternetArchiveDetailsContent( var isRemoving by remember { mutableStateOf(false) } + Box( modifier = Modifier .fillMaxSize() @@ -77,53 +85,44 @@ private fun InternetArchiveDetailsContent( Spacer(Modifier.height(ThemeDimensions.spacing.large)) - Text( - text = stringResource(id = R.string.label_username), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onBackground - ) - Text( - text = state.userName, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onBackground + CustomTextField( + label = stringResource(R.string.label_username), + value = state.userName, + onValueChange = {}, + enabled = false, ) - Text( - modifier = Modifier.padding(top = 16.dp), - text = stringResource(id = R.string.label_screen_name), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onBackground - ) + Spacer(Modifier.height(ThemeDimensions.spacing.medium)) - Text( - text = state.screenName, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onBackground + CustomTextField( + label = stringResource(R.string.label_screen_name), + value = state.screenName, + onValueChange = {}, + enabled = false, ) - Text( - modifier = Modifier.padding(top = 16.dp), - text = stringResource(id = R.string.label_email), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onBackground - ) + Spacer(Modifier.height(ThemeDimensions.spacing.medium)) + - Text( - text = state.email, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onBackground + CustomTextField( + label = stringResource(R.string.label_email), + value = state.email, + onValueChange = {}, + enabled = false, ) } Button( modifier = Modifier - .padding(top = 12.dp) + .padding(12.dp) .align(Alignment.BottomCenter), - shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { isRemoving = true }, - colors = ButtonDefaults.buttonColors(containerColor = ThemeColors.material.error) + colors = ButtonDefaults.buttonColors( + containerColor = ThemeColors.material.error, + contentColor = Color.White + ) ) { Text(stringResource(id = R.string.menu_delete)) } @@ -149,9 +148,11 @@ private fun RemoveInternetArchiveDialog(onDismiss: () -> Unit, onRemove: () -> U }, text = { Text(stringResource(id = R.string.are_you_sure_you_want_to_remove_this_server_from_the_app)) }, dismissButton = { - OutlinedButton( + TextButton( onClick = onDismiss, - shape = RoundedCornerShape(ThemeDimensions.roundedCorner) + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) ) { Text(stringResource(id = R.string.action_cancel)) } @@ -159,7 +160,10 @@ private fun RemoveInternetArchiveDialog(onDismiss: () -> Unit, onRemove: () -> U Button( onClick = onRemove, shape = RoundedCornerShape(ThemeDimensions.roundedCorner), - colors = ButtonDefaults.buttonColors(containerColor = ThemeColors.material.error) + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) ) { Text(stringResource(id = R.string.remove)) } @@ -168,18 +172,24 @@ private fun RemoveInternetArchiveDialog(onDismiss: () -> Unit, onRemove: () -> U @Composable @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) private fun InternetArchiveScreenPreview() { - InternetArchiveDetailsContent( - state = InternetArchiveDetailsState( - email = "abc@example.com", - userName = "@abc_name", - screenName = "ABC Name" - ) - ) {} + DefaultScaffoldPreview { + InternetArchiveDetailsContent( + state = InternetArchiveDetailsState( + email = "abc@example.com", + userName = "@abc_name", + screenName = "ABC Name" + ) + ) {} + } } @Composable @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) private fun RemoveInternetArchiveDialogPreview() { - RemoveInternetArchiveDialog(onDismiss = { }) {} + Theme { + RemoveInternetArchiveDialog(onDismiss = { }) {} + } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt index 65267de5..e305ea14 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -24,30 +25,41 @@ import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.PlatformImeOptions import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.presentation.theme.Theme import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions import net.opendasharchive.openarchive.core.state.Dispatch @@ -59,6 +71,7 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.log import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginAction.UpdatePassword import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginAction.UpdateUsername import org.koin.androidx.compose.koinViewModel +import org.koin.compose.KoinContext import org.koin.core.parameter.parametersOf import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginAction as Action @@ -68,10 +81,11 @@ fun InternetArchiveLoginScreen(space: Space, onResult: (IAResult) -> Unit) { parametersOf(space) } - val state by viewModel.state.collectAsState() + val state by viewModel.state.collectAsStateWithLifecycle() val launcher = - rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult(), + rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), onResult = {}) LaunchedEffect(Unit) { @@ -116,7 +130,7 @@ private fun InternetArchiveLoginContent( Column( modifier = Modifier .fillMaxSize() - .padding(ThemeDimensions.spacing.medium), + .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -124,57 +138,32 @@ private fun InternetArchiveLoginContent( modifier = Modifier.padding(bottom = ThemeDimensions.spacing.large) ) - OutlinedTextField( + CustomTextField( value = state.username, - enabled = !state.isBusy, onValueChange = { dispatch(UpdateUsername(it)) }, - label = { - Text(stringResource(R.string.label_username)) - }, - placeholder = { - Text(stringResource(R.string.placeholder_email_or_username)) - }, - singleLine = true, - shape = RoundedCornerShape(ThemeDimensions.roundedCorner), - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Next, - autoCorrect = false, - keyboardType = KeyboardType.Email - ), + label = stringResource(R.string.label_username), + placeholder = stringResource(R.string.placeholder_email_or_username), isError = state.isUsernameError, + isLoading = state.isBusy, + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next, ) Spacer(Modifier.height(ThemeDimensions.spacing.large)) - OutlinedTextField( + CustomSecureField( value = state.password, - enabled = !state.isBusy, onValueChange = { dispatch(UpdatePassword(it)) }, - label = { - Text(stringResource(R.string.label_password)) - }, - placeholder = { - Text(stringResource(R.string.placeholder_password)) - }, - singleLine = true, - trailingIcon = { - IconButton(modifier = Modifier.sizeIn(ThemeDimensions.touchable), onClick = { showPassword = !showPassword }) { - Icon( - imageVector = if (showPassword) Icons.Default.Visibility else Icons.Default.VisibilityOff, - contentDescription = "show password" - ) - } - }, - shape = RoundedCornerShape(ThemeDimensions.roundedCorner), - visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - autoCorrect = false, - imeAction = ImeAction.Go - ), + label = stringResource(R.string.label_password), + placeholder = stringResource(R.string.placeholder_password), isError = state.isPasswordError, + isLoading = state.isBusy, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, ) + Spacer(Modifier.height(ThemeDimensions.spacing.large)) + AnimatedVisibility( visible = state.isLoginError, enter = fadeIn(), @@ -241,11 +230,162 @@ private fun InternetArchiveLoginContent( } @Composable -@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) private fun InternetArchiveLoginPreview() { - InternetArchiveLoginContent( - state = InternetArchiveLoginState( - username = "user@example.org", password = "abc123" + DefaultScaffoldPreview { + InternetArchiveLoginContent( + state = InternetArchiveLoginState( + username = "user@example.org", password = "abc123" + ) + ) {} + } +} + +@Composable +fun DefaultScaffoldPreview( + content: @Composable () -> Unit +) { + + Theme { + + Scaffold( + topBar = { + ComposeAppBar() + } + ) { paddingValues -> + + Box(modifier = Modifier.padding(paddingValues), contentAlignment = Alignment.Center) { + content() + } + } + } + +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ComposeAppBar( + title: String = "Save App", + onNavigationAction: () -> Unit = {} +) { + TopAppBar( + title = { + Text(title) + }, + navigationIcon = { + IconButton(onClick = onNavigationAction) { + Icon(painter = painterResource(R.drawable.ic_arrow_back), contentDescription = null) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + navigationIconContentColor = Color.White, + titleContentColor = Color.White, + actionIconContentColor = Color.White ) - ) {} + ) +} + +@Composable +fun CustomTextField( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, + label: String, + enabled: Boolean = true, + placeholder: String? = null, + isError: Boolean = false, + isLoading: Boolean = false, + keyboardType: KeyboardType = KeyboardType.Text, + imeAction: ImeAction = ImeAction.Next, +) { + + TextField( + modifier = modifier.fillMaxWidth(), + value = value, + enabled = !isLoading, + onValueChange = onValueChange, + label = { + Text(label) + }, + placeholder = { + placeholder?.let { + Text(placeholder) + } + }, + singleLine = true, + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrectEnabled = false, + keyboardType = keyboardType, + imeAction = imeAction, + platformImeOptions = PlatformImeOptions(), + showKeyboardOnFocus = true, + hintLocales = null + ), + isError = isError, + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) +} + +@Composable +fun CustomSecureField( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, + label: String, + placeholder: String, + isError: Boolean = false, + isLoading: Boolean = false, + keyboardType: KeyboardType, + imeAction: ImeAction, +) { + + var showPassword by rememberSaveable { + mutableStateOf(false) + } + + TextField( + modifier = modifier.fillMaxWidth(), + value = value, + enabled = !isLoading, + onValueChange = onValueChange, + label = { + Text(label) + }, + placeholder = { + Text(placeholder) + }, + singleLine = true, + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrectEnabled = false, + keyboardType = keyboardType, + imeAction = imeAction, + platformImeOptions = PlatformImeOptions(), + showKeyboardOnFocus = true, + hintLocales = null + ), + visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + isError = isError, + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + trailingIcon = { + IconButton( + modifier = Modifier.sizeIn(ThemeDimensions.touchable), + onClick = { showPassword = !showPassword }) { + Icon( + imageVector = if (showPassword) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = "show password" + ) + } + }, + ) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt index d94a18f3..e513219f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt @@ -3,72 +3,62 @@ package net.opendasharchive.openarchive.features.main import android.content.Intent import android.os.Build import android.os.Bundle -import android.view.Menu import android.view.MenuItem import android.view.View +import android.view.ViewTreeObserver +import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import net.opendasharchive.openarchive.FolderAdapter -import net.opendasharchive.openarchive.FolderAdapterListener import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.SpaceAdapter -import net.opendasharchive.openarchive.SpaceAdapterListener import net.opendasharchive.openarchive.databinding.ActivityMainBinding import net.opendasharchive.openarchive.db.Project import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.extensions.getMeasurments import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.features.folders.AddFolderActivity import net.opendasharchive.openarchive.features.media.AddMediaDialogFragment import net.opendasharchive.openarchive.features.media.AddMediaType +import net.opendasharchive.openarchive.features.media.ContentPickerFragment import net.opendasharchive.openarchive.features.media.MediaLaunchers import net.opendasharchive.openarchive.features.media.Picker import net.opendasharchive.openarchive.features.media.PreviewActivity import net.opendasharchive.openarchive.features.onboarding.Onboarding23Activity import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity +import net.opendasharchive.openarchive.features.settings.FoldersActivity +import net.opendasharchive.openarchive.features.settings.SpacesActivity import net.opendasharchive.openarchive.upload.UploadService import net.opendasharchive.openarchive.util.AlertHelper import net.opendasharchive.openarchive.util.Prefs import net.opendasharchive.openarchive.util.ProofModeHelper -import net.opendasharchive.openarchive.util.extensions.Position import net.opendasharchive.openarchive.util.extensions.cloak import net.opendasharchive.openarchive.util.extensions.hide -import net.opendasharchive.openarchive.util.extensions.scaleAndTintDrawable -import net.opendasharchive.openarchive.util.extensions.scaled -import net.opendasharchive.openarchive.util.extensions.setDrawable import net.opendasharchive.openarchive.util.extensions.show import java.text.NumberFormat -import kotlin.math.roundToInt -class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener { +class MainActivity : BaseActivity() { private var mMenuDelete: MenuItem? = null private var mSnackBar: Snackbar? = null - private lateinit var mBinding: ActivityMainBinding + private lateinit var binding: ActivityMainBinding private lateinit var mPagerAdapter: ProjectAdapter - private lateinit var mSpaceAdapter: SpaceAdapter - private lateinit var mFolderAdapter: FolderAdapter private lateinit var mediaLaunchers: MediaLaunchers private var mLastItem: Int = 0 private var mLastMediaItem: Int = 0 - private var serverListOffset: Float = 0F - private var serverListCurOffset: Float = 0F - private var mCurrentItem - get() = mBinding.pager.currentItem + private var mCurrentPagerItem + get() = binding.pager.currentItem set(value) { - mBinding.pager.currentItem = value + binding.pager.currentItem = value updateBottomNavbar(value) } @@ -79,15 +69,27 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener } } + private val folderResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + val selectedFolderId = result.data?.getLongExtra("SELECTED_FOLDER_ID", -1) + if (selectedFolderId != null && selectedFolderId > -1) { + navigateToFolder(selectedFolderId) + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + installSplashScreen() - mBinding = ActivityMainBinding.inflate(layoutInflater) - setContentView(mBinding.root) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) mediaLaunchers = Picker.register( activity = this, - root = mBinding.root, + root = binding.root, project = { getSelectedProject() }, completed = { media -> refreshCurrentProject() @@ -97,18 +99,19 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener } }) - setSupportActionBar(mBinding.toolbar) + setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(false) supportActionBar?.title = null mPagerAdapter = ProjectAdapter(supportFragmentManager, lifecycle) - mBinding.pager.adapter = mPagerAdapter + binding.pager.adapter = mPagerAdapter - mBinding.pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + binding.pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageScrolled( position: Int, positionOffset: Float, positionOffsetPixels: Int ) { + // Do Nothing } override fun onPageSelected(position: Int) { @@ -125,81 +128,62 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener override fun onPageScrollStateChanged(state: Int) {} }) - mBinding.spaceName.setOnClickListener { - var newAlpha = 0F + setupBottomNavBar() - if (serverListCurOffset != serverListOffset) { - serverListCurOffset = serverListOffset - mBinding.spaceName.setDrawable(R.drawable.ic_expand_more, Position.End, 0.75) - } else { - newAlpha = 1F - serverListCurOffset = 0F - mBinding.spaceName.setDrawable(R.drawable.ic_expand_less, Position.End, 0.75) - } - mBinding.spaces.visibility = View.VISIBLE - mBinding.currentSpaceName.visibility = View.VISIBLE - mBinding.newFolder.visibility = View.VISIBLE - mBinding.folders.visibility = View.VISIBLE - - mBinding.spaces.animate().translationY(serverListCurOffset).alpha(newAlpha) - .withEndAction { - run { - if (newAlpha == 0F) { - mBinding.spaces.hide(false) - } - } - } - mBinding.currentSpaceName.animate().alpha(1 - newAlpha) - mBinding.newFolder.animate().alpha(1 - newAlpha) - mBinding.folders.animate().alpha(1 - newAlpha) + binding.breadcrumbSpace.setOnClickListener { + startActivity(Intent(this, SpacesActivity::class.java)) } - updateCurrentSpaceAtDrawer() - - mSpaceAdapter = SpaceAdapter(this) - mBinding.spaces.layoutManager = LinearLayoutManager(this) - mBinding.spaces.adapter = mSpaceAdapter - - mFolderAdapter = FolderAdapter(this) - mBinding.folders.layoutManager = LinearLayoutManager(this) - mBinding.folders.adapter = mFolderAdapter - - mBinding.newFolder.scaleAndTintDrawable(Position.Start, 0.75) - mBinding.newFolder.setOnClickListener { - addFolder() + binding.breadcrumbFolder.setOnClickListener { + val selectedSpaceId = getSelectedSpace()?.id + val selectedProjectId = getSelectedProject()?.id + val intent = Intent(this, FoldersActivity::class.java) + intent.putExtra( + FoldersActivity.EXTRA_SELECTED_SPACE_ID, + selectedSpaceId + ) // Pass the selected space ID + intent.putExtra( + FoldersActivity.EXTRA_SELECTED_PROJECT_ID, + selectedProjectId + ) // Pass the selected project ID + folderResultLauncher.launch(intent) } + } - mBinding.myMediaButton.setOnClickListener { - mCurrentItem = mLastMediaItem - } - mBinding.myMediaLabel.setOnClickListener { - // perform click + play ripple animation - mBinding.myMediaButton.isPressed = true - mBinding.myMediaButton.isPressed = false - mBinding.myMediaButton.performClick() + private fun setupBottomNavBar() { + binding.bottomNavBar.onMyMediaClick = { + mCurrentPagerItem = mLastMediaItem } - mBinding.addButton.setOnClickListener { - addClicked(AddMediaType.GALLERY) - } + binding.bottomNavBar.onAddClick = { - mBinding.settingsButton.setOnClickListener { - mCurrentItem = mPagerAdapter.settingsIndex + if (Prefs.addMediaHint) { + addClicked(AddMediaType.GALLERY) + } else { + AlertHelper.show( + context = this, + message = R.string.press_and_hold_options_media_screen_message, + title = R.string.press_and_hold_options_media_screen_title, + ) + Prefs.addMediaHint = true + } } - mBinding.settingsLabel.setOnClickListener { - // perform click + play ripple animation - mBinding.settingsButton.isPressed = true - mBinding.settingsButton.isPressed = false - mBinding.settingsButton.performClick() + + binding.bottomNavBar.onSettingsClick = { + mCurrentPagerItem = mPagerAdapter.settingsIndex } if (Picker.canPickFiles(this)) { - mBinding.addButton.setOnLongClickListener { - val addMediaDialogFragment = AddMediaDialogFragment() - addMediaDialogFragment.show(supportFragmentManager, addMediaDialogFragment.tag) + binding.bottomNavBar.setAddButtonLongClickEnabled() - true + binding.bottomNavBar.onAddLongClick = { + //val addMediaDialogFragment = AddMediaDialogFragment() + //addMediaDialogFragment.show(supportFragmentManager, addMediaDialogFragment.tag) + + val addMediaBottomSheet = + ContentPickerFragment { actionType -> addClicked(actionType) } + addMediaBottomSheet.show(supportFragmentManager, ContentPickerFragment.TAG) } supportFragmentManager.setFragmentResultListener( @@ -223,6 +207,18 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener } } + private fun updateBottomNavbar(position: Int) { + binding.bottomNavBar.updateSelectedItem(isSettings = position == mPagerAdapter.settingsIndex) + if (position == mPagerAdapter.settingsIndex) { + binding.breadcrumbContainer.hide() + } else { + // Show the breadcrumb container only if there's any server available + if (Space.current != null) { + binding.breadcrumbContainer.show() + } + } + } + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) override fun onStart() { super.onStart() @@ -238,56 +234,27 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener refreshSpace() - mCurrentItem = mLastItem + mCurrentPagerItem = mLastItem if (!Prefs.didCompleteOnboarding) { startActivity(Intent(this, Onboarding23Activity::class.java)) } importSharedMedia(intent) - - if (serverListOffset == 0F) { - val dims = mBinding.spaces.getMeasurments() - serverListOffset = -dims.second.toFloat() - serverListCurOffset = serverListOffset - mBinding.spaces.visibility = View.GONE - mBinding.spaces.animate().translationY(serverListOffset) - mBinding.spaceName.setDrawable(R.drawable.ic_expand_more, Position.End, 0.75) - } } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_main, menu) - mMenuDelete = menu.findItem(R.id.menu_delete) - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.menu_folders -> { + private fun navigateToFolder(folderId: Long) { + val folderIndex = mPagerAdapter.getProjectIndexById(folderId) + if (folderIndex >= 0) { + binding.pager.setCurrentItem(folderIndex, true) + mCurrentPagerItem = folderIndex - if (mBinding.root.isDrawerOpen(mBinding.folderBar)) { - mBinding.root.closeDrawer(mBinding.folderBar) - } else { - mBinding.root.openDrawer(mBinding.folderBar) - } - true - } - - else -> super.onOptionsItemSelected(item) + } else { + Toast.makeText(this, "Folder not found", Toast.LENGTH_SHORT).show() } } - private fun updateCurrentSpaceAtDrawer() { - mBinding.currentSpaceName.text = Space.current?.friendlyName - mBinding.currentSpaceName.setDrawable( - Space.current?.getAvatar(applicationContext)?.scaled(32, applicationContext), - Position.Start, tint = true - ) - mBinding.currentSpaceName.compoundDrawablePadding = - applicationContext.resources.getDimension(R.dimen.padding_small).roundToInt() - } fun updateAfterDelete(done: Boolean) { mMenuDelete?.isVisible = !done @@ -297,25 +264,16 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener private fun addFolder() { mNewFolderResultLauncher.launch(Intent(this, AddFolderActivity::class.java)) - - mBinding.root.closeDrawer(mBinding.folderBar) } private fun refreshSpace() { -// val currentSpace = Space.current - -// if (currentSpace != null) { -// mBinding.space.setDrawable( -// currentSpace.getAvatar(this@MainActivity) -// ?.scaled(32, this@MainActivity), Position.Start, tint = false -// ) - mBinding.spaceName.text = getString(R.string.servers) // currentSpace.friendlyName -// } else { -// mBinding.space.setDrawable(R.drawable.avatar_default, Position.Start, tint = false) -// mBinding.space.text = getString(R.string.app_name) -// } - - mSpaceAdapter.update(Space.getAll().asSequence().toList()) + val currentSpace = Space.current + currentSpace?.let { space -> + binding.breadcrumbSpace.text = space.friendlyName + space.setAvatar(binding.spaceIcon) + } ?: run { + binding.breadcrumbContainer.visibility = View.INVISIBLE + } refreshProjects() } @@ -325,33 +283,27 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener mPagerAdapter.updateData(projects) - mBinding.pager.adapter = mPagerAdapter + binding.pager.adapter = mPagerAdapter setProjectId?.let { - mCurrentItem = mPagerAdapter.getProjectIndexById(it, default = 0) + mCurrentPagerItem = mPagerAdapter.getProjectIndexById(it, default = 0) } - - mFolderAdapter.update(projects) - - refreshCurrentProject() } private fun refreshCurrentProject() { val project = getSelectedProject() if (project != null) { - mBinding.pager.post { + binding.pager.post { mPagerAdapter.notifyProjectChanged(project) } - project.space?.setAvatar(mBinding.currentFolderIcon) - mBinding.currentFolderIcon.show() + project.space?.setAvatar(binding.spaceIcon) + binding.breadcrumbFolder.text = project.description + binding.breadcrumbFolder.show() - mBinding.currentFolderName.text = project.description - mBinding.currentFolderName.show() } else { - mBinding.currentFolderIcon.cloak() - mBinding.currentFolderName.cloak() + this@MainActivity.binding.breadcrumbFolder.cloak() } refreshCurrentFolderCount() @@ -361,24 +313,24 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener val project = getSelectedProject() if (project != null) { - mBinding.currentFolderCount.text = NumberFormat.getInstance().format( + val count = NumberFormat.getInstance().format( project.collections.map { it.size } .reduceOrNull { acc, count -> acc + count } ?: 0) - mBinding.currentFolderCount.show() -// mBinding.uploadEditButton.toggle(project.isUploading) + binding.folderCount.text = count + binding.folderCount.show() + } else { - mBinding.currentFolderCount.cloak() -// mBinding.uploadEditButton.hide() + binding.folderCount.cloak() } } - private fun importSharedMedia(data: Intent?) { - if (data?.action != Intent.ACTION_SEND) return + private fun importSharedMedia(imageIntent: Intent?) { + if (imageIntent?.action != Intent.ACTION_SEND) return - val uri = data.data ?: if ((data.clipData?.itemCount + val uri = imageIntent.data ?: if ((imageIntent.clipData?.itemCount ?: 0) > 0 - ) data.clipData?.getItemAt(0)?.uri else null + ) imageIntent.clipData?.getItemAt(0)?.uri else null val path = uri?.path ?: return if (path.contains(packageName)) return @@ -417,64 +369,30 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener } } -// private fun showAlertIcon() { -// mBinding.alertIcon.show() -// TooltipCompat.setTooltipText( -// mBinding.alertIcon, -// getString(R.string.unsecured_internet_connection) -// ) -// } - - override fun projectClicked(project: Project) { - mCurrentItem = mPagerAdapter.projects.indexOf(project) - -// mBinding.root.closeDrawer(mBinding.folderBar) - -// mBinding.spacesCard.disableAnimation { -// mBinding.spacesCard.hide() -// } - // make sure that even when navigating to settings and picking a folder there - // the dataset will get update correctly - mFolderAdapter.notifyDataSetChanged() + fun getSelectedProject(): Project? { + return mPagerAdapter.getProject(mCurrentPagerItem) } - override fun getSelectedProject(): Project? { - return mPagerAdapter.getProject(mCurrentItem) - } - - override fun spaceClicked(space: Space) { - Space.current = space - - refreshSpace() - - mBinding.root.closeDrawer(mBinding.folderBar) - -// mBinding.spacesCard.disableAnimation { -// mBinding.spacesCard.hide() -// } - - updateCurrentSpaceAtDrawer() - } - - override fun addSpaceClicked() { - mBinding.root.closeDrawer(mBinding.folderBar) - - startActivity(Intent(this, SpaceSetupActivity::class.java)) - } - - override fun getSelectedSpace(): Space? { + fun getSelectedSpace(): Space? { return Space.current } private fun addClicked(mediaType: AddMediaType) { + // Check if there's any project selected if (getSelectedProject() != null) { - when(mediaType) { - AddMediaType.CAMERA -> Picker.takePhoto(this@MainActivity, mediaLaunchers.cameraLauncher) + when (mediaType) { + AddMediaType.CAMERA -> Picker.takePhoto( + this@MainActivity, + mediaLaunchers.cameraLauncher + ) + AddMediaType.GALLERY -> Picker.pickMedia(this, mediaLaunchers.imagePickerLauncher) AddMediaType.FILES -> Picker.pickFiles(mediaLaunchers.filePickerLauncher) } + } else if (Space.current == null) { // Check if there's any space available + startActivity(Intent(this, SpaceSetupActivity::class.java)) } else { @@ -498,14 +416,4 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener } } } - - private fun updateBottomNavbar(position: Int) { - if (position == mPagerAdapter.settingsIndex) { - mBinding.myMediaButton.setIconResource(R.drawable.outline_perm_media_24) - mBinding.settingsButton.setIconResource(R.drawable.ic_settings_filled) - } else { - mBinding.myMediaButton.setIconResource(R.drawable.perm_media_24px) - mBinding.settingsButton.setIconResource(R.drawable.ic_settings) - } - } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt index 2e6e6293..b64b3647 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt @@ -24,6 +24,7 @@ import net.opendasharchive.openarchive.db.Collection import net.opendasharchive.openarchive.db.Media import net.opendasharchive.openarchive.db.MediaAdapter import net.opendasharchive.openarchive.db.MediaViewHolder +import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.upload.BroadcastManager import net.opendasharchive.openarchive.util.AlertHelper import net.opendasharchive.openarchive.util.extensions.toggle @@ -128,6 +129,22 @@ class MainMediaFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + if (mProjectId == -1L) { + val space = Space.current + val text: String = if (space != null) { + val projects = space.projects + if (projects.isNotEmpty()) { + getString(R.string.tap_to_add) + } else { + "Tap the button below to add media folder." + } + } else { + "Tap the button below to add media server." + } + + binding.tvWelcomeDescr.text = text + } + refresh() } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt index 78402f26..1cf0d1b5 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt @@ -37,8 +37,7 @@ data class SectionViewHolder( collection: Collection, media: List ) { - if (media.any { it.isUploading }) - { + if (media.any { it.isUploading }) { timestamp.setText(R.string.uploading) val uploaded = media.filter { it.sStatus == Media.Status.Uploaded }.size diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/media/AddMediaDialogFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/media/AddMediaDialogFragment.kt index 1a3a7f8a..8d385aab 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/media/AddMediaDialogFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/media/AddMediaDialogFragment.kt @@ -18,7 +18,7 @@ class AddMediaDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val builder = MaterialAlertDialogBuilder(requireContext()) - mDialogView = onCreateView(LayoutInflater.from(requireContext()), null, savedInstanceState) + mDialogView = onCreateView(layoutInflater, null, savedInstanceState) builder.setView(mDialogView) return builder.create() } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/media/ContentPickerFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/media/ContentPickerFragment.kt new file mode 100644 index 00000000..1ea462a8 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/media/ContentPickerFragment.kt @@ -0,0 +1,46 @@ +package net.opendasharchive.openarchive.features.media + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import net.opendasharchive.openarchive.databinding.FragmentContentPickerBinding + +class ContentPickerFragment(private val onMediaPicked: (AddMediaType) -> Unit): BottomSheetDialogFragment() { + + private var _binding: FragmentContentPickerBinding? = null + private val binding get() = _binding!! + + + companion object { + const val TAG = "ModalBottomSheet-ContentPickerFragment" + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentContentPickerBinding.inflate(inflater, container, false) + + + binding.actionUploadCamera.setOnClickListener { + onMediaPicked(AddMediaType.CAMERA) + dismiss() + } + + binding.actionUploadMedia.setOnClickListener { + onMediaPicked(AddMediaType.GALLERY) + dismiss() + } + + binding.actionUploadFiles.setOnClickListener { + onMediaPicked(AddMediaType.FILES) + dismiss() + } + + + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewActivity.kt index 2e03d24f..e332f4da 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewActivity.kt @@ -72,9 +72,10 @@ class PreviewActivity : BaseActivity(), View.OnClickListener, PreviewAdapter.Lis refresh() }) - setSupportActionBar(mBinding.toolbar) - supportActionBar?.title = getString(R.string.preview_media) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + setupToolbar( + title = getString(R.string.preview_media), + showBackButton = true + ) mBinding.mediaGrid.layoutManager = GridLayoutManager(this, 2) mBinding.mediaGrid.adapter = PreviewAdapter(this) @@ -87,8 +88,8 @@ class PreviewActivity : BaseActivity(), View.OnClickListener, PreviewAdapter.Lis if (Picker.canPickFiles(this)) { mBinding.btAddMore.setOnLongClickListener { - mBinding.addMenu.container.show(animate = true) - + //mBinding.addMenu.container.show(animate = true) + initAddMediaBottomSheet() true } @@ -121,6 +122,20 @@ class PreviewActivity : BaseActivity(), View.OnClickListener, PreviewAdapter.Lis refresh() } + private fun initAddMediaBottomSheet() { + + if (Picker.canPickFiles(this)) { + val modalBottomSheet = ContentPickerFragment { action -> + when (action) { + AddMediaType.CAMERA -> Picker.takePhoto(this@PreviewActivity, mediaLaunchers.cameraLauncher) + AddMediaType.FILES -> Picker.pickFiles(mediaLaunchers.filePickerLauncher) + AddMediaType.GALLERY -> onClick(mBinding.btAddMore) + } + } + modalBottomSheet.show(supportFragmentManager, ContentPickerFragment.TAG) + } + } + override fun onResume() { super.onResume() @@ -129,56 +144,13 @@ class PreviewActivity : BaseActivity(), View.OnClickListener, PreviewAdapter.Lis override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_preview, menu) - return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - android.R.id.home -> { - finish() - - return true - } - R.id.menu_upload -> { - val queue = { - mMedia.forEach { - it.sStatus = Media.Status.Queued - it.selected = false - it.save() - } - - finish() - } - - if (Prefs.dontShowUploadHint) { - queue() - } else { - var doNotShowAgain = false - - val d = AlertDialog.Builder(ContextThemeWrapper(this, R.style.AlertDialogTheme)) - .setTitle(R.string.once_uploaded_you_will_not_be_able_to_edit_media) - .setIcon(R.drawable.baseline_cloud_upload_black_48) - .setPositiveButton( - R.string.got_it - ) { _: DialogInterface, _: Int -> - Prefs.dontShowUploadHint = doNotShowAgain - queue() - } - .setNegativeButton(R.string.lbl_Cancel) { dialog: DialogInterface, _: Int -> dialog.dismiss() } - .setMultiChoiceItems( - arrayOf(getString(R.string.do_not_show_me_this_again)), - booleanArrayOf(false) - ) - { _, _, isChecked -> - doNotShowAgain = isChecked - }.show() - - // hack for making sure this dialog always shows all lines of the pretty long title, even on small screens - d.findViewById(androidx.appcompat.R.id.alertTitle)?.maxLines = 99 - - } + uploadMedia() return true } } @@ -262,4 +234,44 @@ class PreviewActivity : BaseActivity(), View.OnClickListener, PreviewAdapter.Lis Prefs.batchHintShown = true } + + private fun uploadMedia() { + val queue = { + mMedia.forEach { + it.sStatus = Media.Status.Queued + it.selected = false + it.save() + } + + finish() + } + + if (Prefs.dontShowUploadHint) { + queue() + } else { + var doNotShowAgain = false + + val d = AlertDialog.Builder(ContextThemeWrapper(this, R.style.AlertDialogTheme)) + .setTitle(R.string.once_uploaded_you_will_not_be_able_to_edit_media) + .setIcon(R.drawable.baseline_cloud_upload_black_48) + .setPositiveButton( + R.string.got_it + ) { _: DialogInterface, _: Int -> + Prefs.dontShowUploadHint = doNotShowAgain + queue() + } + .setNegativeButton(R.string.lbl_Cancel) { dialog: DialogInterface, _: Int -> dialog.dismiss() } + .setMultiChoiceItems( + arrayOf(getString(R.string.do_not_show_me_this_again)), + booleanArrayOf(false) + ) + { _, _, isChecked -> + doNotShowAgain = isChecked + }.show() + + // hack for making sure this dialog always shows all lines of the pretty long title, even on small screens + d.findViewById(androidx.appcompat.R.id.alertTitle)?.maxLines = 99 + + } + } } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/media/ReviewActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/media/ReviewActivity.kt index 691938e7..370f6cf9 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/media/ReviewActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/media/ReviewActivity.kt @@ -10,7 +10,6 @@ import android.view.MenuItem import android.view.View import android.widget.ImageView import com.bumptech.glide.Glide -import com.facebook.drawee.view.SimpleDraweeView import com.github.derlio.waveform.SimpleWaveformView import com.squareup.picasso.Picasso import net.opendasharchive.openarchive.R @@ -67,8 +66,10 @@ class ReviewActivity : BaseActivity(), View.OnClickListener { mBinding = ActivityReviewBinding.inflate(layoutInflater) setContentView(mBinding.root) - setSupportActionBar(mBinding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + setupToolbar( + title = getString(R.string.edit_media_info), + showBackButton = true + ) mStore = intent.getLongArrayExtra(EXTRA_CURRENT_MEDIA_ID) ?.map { Media.get(it) }?.filterNotNull() ?: emptyList() @@ -166,12 +167,12 @@ class ReviewActivity : BaseActivity(), View.OnClickListener { override fun onClick(view: View?) { when (view) { - mBinding.waveform, mBinding.image -> { - if (mMedia?.mimeType?.startsWith("image") == true) { - val draweeView = SimpleDraweeView(this) - draweeView.setImageURI(mMedia?.fileUri) - } - } +// mBinding.waveform, mBinding.image -> { +// if (mMedia?.mimeType?.startsWith("image") == true) { +// val draweeView = SimpleDraweeView(this) +// draweeView.setImageURI(mMedia?.fileUri) +// } +// } mBinding.btFlag -> { showFirstTimeFlag() @@ -285,7 +286,11 @@ class ReviewActivity : BaseActivity(), View.OnClickListener { private fun showFirstTimeFlag() { if (Prefs.flagHintShown) return - AlertHelper.show(this, R.string.popup_flag_desc, R.string.popup_flag_title) + AlertHelper.show( + context = this, + message = R.string.popup_flag_desc, + title = R.string.popup_flag_title + ) Prefs.flagHintShown = true } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23Activity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23Activity.kt index e5d63df1..1ce30fdf 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23Activity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23Activity.kt @@ -45,7 +45,7 @@ class Onboarding23Activity : BaseActivity() { mBinding.titleBlock.verifyText, mBinding.titleBlock.encryptText )) { - textView.text = colorizeFirstLetter(textView.text, R.color.colorPrimary) + textView.text = colorizeFirstLetter(textView.text, R.color.colorTertiary) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23InstructionsActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23InstructionsActivity.kt index 1197cf54..49e8f42e 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23InstructionsActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23InstructionsActivity.kt @@ -12,6 +12,7 @@ import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.databinding.ActivityOnboarding23InstructionsBinding import net.opendasharchive.openarchive.features.core.BaseActivity +import net.opendasharchive.openarchive.features.main.MainActivity import net.opendasharchive.openarchive.util.Prefs class Onboarding23InstructionsActivity : BaseActivity() { @@ -119,6 +120,8 @@ class Onboarding23InstructionsActivity : BaseActivity() { private fun done() { Prefs.didCompleteOnboarding = true - startActivity(Intent(this, SpaceSetupActivity::class.java)) + // We are moving space setup to MainActivity + startActivity(Intent(this, MainActivity::class.java)) + finish() } } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt index f45f3b55..576d822e 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt @@ -2,17 +2,30 @@ package net.opendasharchive.openarchive.features.onboarding import android.content.Intent import android.os.Bundle +import androidx.fragment.app.Fragment import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.databinding.ActivitySpaceSetupBinding import net.opendasharchive.openarchive.features.core.BaseActivity +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveFragment import net.opendasharchive.openarchive.features.main.MainActivity import net.opendasharchive.openarchive.features.settings.SpaceSetupFragment import net.opendasharchive.openarchive.features.settings.SpaceSetupSuccessFragment import net.opendasharchive.openarchive.services.gdrive.GDriveFragment -import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveFragment -import net.opendasharchive.openarchive.services.webdav.WebDavSetupLicenseFragment -import net.opendasharchive.openarchive.services.internetarchive.Util import net.opendasharchive.openarchive.services.webdav.WebDavFragment +import net.opendasharchive.openarchive.services.webdav.WebDavSetupLicenseFragment + +interface ToolbarConfigurable { + fun getToolbarTitle(): String + fun getToolbarSubtitle(): String? = null + fun shouldShowBackButton(): Boolean = true +} + +abstract class BaseFragment : Fragment(), ToolbarConfigurable { + override fun onResume() { + super.onResume() + (activity as? SpaceSetupActivity)?.updateToolbarFromFragment(this) + } +} class SpaceSetupActivity : BaseActivity() { @@ -28,6 +41,11 @@ class SpaceSetupActivity : BaseActivity() { mBinding = ActivitySpaceSetupBinding.inflate(layoutInflater) setContentView(mBinding.root) + setupToolbar( + title = "Servers", + showBackButton = true + ) + initSpaceSetupFragmentBindings() initWebDavFragmentBindings() initWebDavCreativeLicenseBindings() @@ -36,6 +54,20 @@ class SpaceSetupActivity : BaseActivity() { initGDriveFragmentBindings() } + fun updateToolbarFromFragment(fragment: Fragment) { + if (fragment is ToolbarConfigurable) { + val title = fragment.getToolbarTitle() + val subtitle = fragment.getToolbarSubtitle() + val showBackButton = fragment.shouldShowBackButton() + setupToolbar(title = title, showBackButton = showBackButton) + supportActionBar?.subtitle = subtitle + } else { + // Default toolbar configuration if fragment doesn't implement interface + setupToolbar(title = "Servers", showBackButton = true) + supportActionBar?.subtitle = null + } + } + private fun initSpaceSetupSuccessFragmentBindings() { supportFragmentManager.setFragmentResultListener(SpaceSetupSuccessFragment.RESP_DONE, this) { _, _ -> finishAffinity() @@ -50,7 +82,6 @@ class SpaceSetupActivity : BaseActivity() { private fun initWebDavFragmentBindings() { supportFragmentManager.setFragmentResultListener(WebDavFragment.RESP_SAVED, this) { key, bundle -> val spaceId = bundle.getLong(WebDavFragment.ARG_SPACE_ID) - progress3() supportFragmentManager .beginTransaction() .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left) @@ -64,7 +95,6 @@ class SpaceSetupActivity : BaseActivity() { supportFragmentManager.setFragmentResultListener(WebDavFragment.RESP_CANCEL, this) { _, _ -> - progress1() supportFragmentManager .beginTransaction() .setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right) @@ -79,7 +109,6 @@ class SpaceSetupActivity : BaseActivity() { */ private fun initWebDavCreativeLicenseBindings() { supportFragmentManager.setFragmentResultListener(WebDavSetupLicenseFragment.RESP_SAVED, this) { _, _ -> - progress4() val message = getString(R.string.you_have_successfully_connected_to_a_private_server) supportFragmentManager .beginTransaction() @@ -93,7 +122,6 @@ class SpaceSetupActivity : BaseActivity() { } supportFragmentManager.setFragmentResultListener(WebDavSetupLicenseFragment.RESP_CANCEL, this) { _, _ -> - progress3() supportFragmentManager .beginTransaction() .setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right) @@ -106,7 +134,6 @@ class SpaceSetupActivity : BaseActivity() { supportFragmentManager.setFragmentResultListener(SpaceSetupFragment.RESULT_REQUEST_KEY, this) { _, bundle -> when (bundle.getString(SpaceSetupFragment.RESULT_BUNDLE_KEY)) { SpaceSetupFragment.RESULT_VAL_INTERNET_ARCHIVE -> { - progress2() supportFragmentManager .beginTransaction() .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left) @@ -119,7 +146,6 @@ class SpaceSetupActivity : BaseActivity() { } SpaceSetupFragment.RESULT_VAL_WEBDAV -> { - progress2() supportFragmentManager .beginTransaction() .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left) @@ -132,7 +158,6 @@ class SpaceSetupActivity : BaseActivity() { } SpaceSetupFragment.RESULT_VAL_GDRIVE -> { - progress2() supportFragmentManager .beginTransaction() .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left) @@ -145,7 +170,6 @@ class SpaceSetupActivity : BaseActivity() { private fun initInternetArchiveFragmentBindings() { supportFragmentManager.setFragmentResultListener(InternetArchiveFragment.RESP_SAVED, this) { _, _ -> - progress4() supportFragmentManager .beginTransaction() .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left) @@ -161,7 +185,6 @@ class SpaceSetupActivity : BaseActivity() { InternetArchiveFragment.RESP_CANCEL, this ) { _, _ -> - progress1() supportFragmentManager .beginTransaction() .setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right) @@ -175,7 +198,6 @@ class SpaceSetupActivity : BaseActivity() { GDriveFragment.RESP_CANCEL, this ) { _, _ -> - progress1() supportFragmentManager .beginTransaction() .setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right) @@ -187,7 +209,6 @@ class SpaceSetupActivity : BaseActivity() { GDriveFragment.RESP_AUTHENTICATED, this ) { _, _ -> - progress4() supportFragmentManager .beginTransaction() .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left) @@ -207,44 +228,4 @@ class SpaceSetupActivity : BaseActivity() { onActivityResult(requestCode, resultCode, data) } } - - private fun progress1() { - Util.setBackgroundTint(mBinding.progressBlock.dot1, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.bar1, R.color.colorSpaceSetupProgressOff) - Util.setBackgroundTint(mBinding.progressBlock.dot2, R.color.colorSpaceSetupProgressOff) - Util.setBackgroundTint(mBinding.progressBlock.bar2, R.color.colorSpaceSetupProgressOff) - Util.setBackgroundTint(mBinding.progressBlock.dot3, R.color.colorSpaceSetupProgressOff) - Util.setBackgroundTint(mBinding.progressBlock.bar3, R.color.colorSpaceSetupProgressOff) - Util.setBackgroundTint(mBinding.progressBlock.dot4, R.color.colorSpaceSetupProgressOff) - } - - private fun progress2() { - Util.setBackgroundTint(mBinding.progressBlock.dot1, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.bar1, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.dot2, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.bar2, R.color.colorSpaceSetupProgressOff) - Util.setBackgroundTint(mBinding.progressBlock.dot3, R.color.colorSpaceSetupProgressOff) - Util.setBackgroundTint(mBinding.progressBlock.bar3, R.color.colorSpaceSetupProgressOff) - Util.setBackgroundTint(mBinding.progressBlock.dot4, R.color.colorSpaceSetupProgressOff) - } - - private fun progress3() { - Util.setBackgroundTint(mBinding.progressBlock.dot1, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.bar1, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.dot2, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.bar2, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.dot3, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.bar3, R.color.colorSpaceSetupProgressOff) - Util.setBackgroundTint(mBinding.progressBlock.dot4, R.color.colorSpaceSetupProgressOff) - } - - private fun progress4() { - Util.setBackgroundTint(mBinding.progressBlock.dot1, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.bar1, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.dot2, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.bar2, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.dot3, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.bar3, R.color.colorSpaceSetupProgressOn) - Util.setBackgroundTint(mBinding.progressBlock.dot4, R.color.colorSpaceSetupProgressOn) - } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/ConsentActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/ConsentActivity.kt index ecb1dad3..69b11260 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/ConsentActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/ConsentActivity.kt @@ -18,8 +18,7 @@ class ConsentActivity: BaseActivity() { mBinding = ActivityConsentBinding.inflate(layoutInflater) setContentView(mBinding.root) - setSupportActionBar(mBinding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + setupToolbar(getString(R.string.health_checks)) mBinding.explainer.text = getString( R.string.by_allowing_health_checks_you_give_permission_for_the_app_to_securely_send_health_check_data_to_the_s_team, diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderActivity.kt index 2082651a..004e7ce3 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderActivity.kt @@ -31,8 +31,8 @@ class EditFolderActivity : BaseActivity() { mBinding = ActivityEditFolderBinding.inflate(layoutInflater) setContentView(mBinding.root) - setSupportActionBar(mBinding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + + setupToolbar("Edit Folder") mBinding.folderName.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { @@ -44,6 +44,9 @@ class EditFolderActivity : BaseActivity() { supportActionBar?.title = newName mBinding.folderName.hint = newName + + + setupToolbar(newName) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/FoldersActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/FoldersActivity.kt index 94e6e959..35c43879 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/FoldersActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/FoldersActivity.kt @@ -2,7 +2,9 @@ package net.opendasharchive.openarchive.features.settings import android.content.Intent import android.os.Bundle +import android.view.Menu import android.view.MenuItem +import android.view.View import androidx.recyclerview.widget.LinearLayoutManager import net.opendasharchive.openarchive.FolderAdapter import net.opendasharchive.openarchive.FolderAdapterListener @@ -11,71 +13,125 @@ import net.opendasharchive.openarchive.databinding.ActivityFoldersBinding import net.opendasharchive.openarchive.db.Project import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseActivity -import net.opendasharchive.openarchive.util.extensions.hide -import net.opendasharchive.openarchive.util.extensions.toggle +import net.opendasharchive.openarchive.features.folders.AddFolderActivity -class FoldersActivity: BaseActivity(), FolderAdapterListener { +class FoldersActivity : BaseActivity(), FolderAdapterListener { companion object { const val EXTRA_SHOW_ARCHIVED = "show_archived" + const val EXTRA_SELECTED_SPACE_ID = "selected_space_id" + const val EXTRA_SELECTED_PROJECT_ID = "SELECTED_PROJECT_ID" } private lateinit var mBinding: ActivityFoldersBinding private lateinit var mAdapter: FolderAdapter private var mArchived = false + private var mSelectedSpaceId = -1L + private var mSelectedProjectId: Long = -1L override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mArchived = intent.getBooleanExtra(EXTRA_SHOW_ARCHIVED, false) + mSelectedSpaceId = intent.getLongExtra(EXTRA_SELECTED_SPACE_ID, -1L) + mSelectedProjectId = intent.getLongExtra(EXTRA_SELECTED_PROJECT_ID, -1L) mBinding = ActivityFoldersBinding.inflate(layoutInflater) setContentView(mBinding.root) - setSupportActionBar(mBinding.toolbar) - supportActionBar?.title = getString(if (mArchived) R.string.archived_folders else R.string.folders) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + setupToolbar( + title = getString(if (mArchived) R.string.archived_folders else R.string.folders), + showBackButton = true + ) - mAdapter = FolderAdapter(this) + setupRecyclerView() + setupButtons() + } + + private fun setupRecyclerView() { + mAdapter = FolderAdapter(context = this, listener = this, isArchived = mArchived) mBinding.rvProjects.layoutManager = LinearLayoutManager(this) mBinding.rvProjects.adapter = mAdapter + } - mBinding.btViewArchived.toggle(!mArchived) - mBinding.btViewArchived.setOnClickListener { - val i = Intent(this, FoldersActivity::class.java) - i.putExtra(EXTRA_SHOW_ARCHIVED, true) - - startActivity(i) + private fun setupButtons() { + mBinding.fabAdd.apply { + visibility = if (mArchived) View.INVISIBLE else View.VISIBLE + setOnClickListener { addFolder() } } } override fun onResume() { super.onResume() + refreshProjects() + invalidateOptionsMenu() + } - val projects = if (mArchived) Space.current?.archivedProjects else Space.current?.projects + private fun refreshProjects() { + val projects = if (mArchived) { + Space.current?.archivedProjects + } else { + Space.current?.projects?.filter { !it.isArchived } + } ?: emptyList() - mAdapter.update(projects ?: emptyList()) + mAdapter.update(projects) + } + + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_folder_list, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + val archivedCount = Space.get(mSelectedSpaceId)?.archivedProjects?.size ?: 0 + menu?.findItem(R.id.action_archived_folders)?.isVisible = (!mArchived && archivedCount > 0) + return super.onPrepareOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - finish() - return true - } + return when (item.itemId) { - return super.onOptionsItemSelected(item) + R.id.action_archived_folders -> { + navigateToArchivedFolders() + true + } + + else -> super.onOptionsItemSelected(item) + } } - override fun projectClicked(project: Project) { - val i = Intent(this, EditFolderActivity::class.java) - i.putExtra(EditFolderActivity.EXTRA_CURRENT_PROJECT_ID, project.id) + private fun navigateToArchivedFolders() { + val intent = Intent(this, FoldersActivity::class.java).apply { + putExtra(EXTRA_SHOW_ARCHIVED, true) + putExtra(EXTRA_SELECTED_SPACE_ID, mSelectedSpaceId) + putExtra(EXTRA_SELECTED_PROJECT_ID, mSelectedProjectId) + } + startActivity(intent) + } - startActivity(i) + private fun addFolder() { + val intent = Intent(this, AddFolderActivity::class.java) + startActivity(intent) } override fun getSelectedProject(): Project? { - return null + return Space.current?.projects?.find { it.id == mSelectedProjectId } + } + + override fun projectClicked(project: Project) { + val resultIntent = Intent() + resultIntent.putExtra("SELECTED_FOLDER_ID", project.id) + setResult(RESULT_OK, resultIntent) + finish() // Close FoldersActivity and return to MainActivity + } + + override fun projectEdit(project: Project) { + val intent = Intent(this, EditFolderActivity::class.java).apply { + putExtra(EditFolderActivity.EXTRA_CURRENT_PROJECT_ID, project.id) + } + startActivity(intent) } } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/GeneralSettingsActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/GeneralSettingsActivity.kt index db392f4b..42b10140 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/GeneralSettingsActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/GeneralSettingsActivity.kt @@ -158,8 +158,7 @@ class GeneralSettingsActivity: BaseActivity() { mBinding = ActivitySettingsContainerBinding.inflate(layoutInflater) setContentView(mBinding.root) - setSupportActionBar(mBinding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + setupToolbar(title = getString(R.string.general)) supportFragmentManager .beginTransaction() diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeSettingsActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeSettingsActivity.kt index 567ba40f..000b96fc 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeSettingsActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeSettingsActivity.kt @@ -151,8 +151,8 @@ class ProofModeSettingsActivity: BaseActivity() { mBinding = ActivitySettingsContainerBinding.inflate(layoutInflater) setContentView(mBinding.root) - setSupportActionBar(mBinding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + setupToolbar(getString(R.string.proofmode)) + supportFragmentManager .beginTransaction() diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt index 10d63fa4..57c841e3 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt @@ -1,120 +1,193 @@ package net.opendasharchive.openarchive.features.settings +import android.app.Activity +import android.app.AlertDialog import android.content.Intent import android.os.Bundle import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.presentation.theme.Theme import net.opendasharchive.openarchive.databinding.FragmentSettingsBinding import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveActivity +import net.opendasharchive.openarchive.features.settings.passcode.PasscodeRepository +import net.opendasharchive.openarchive.features.settings.passcode.passcode_setup.PasscodeSetupActivity import net.opendasharchive.openarchive.services.gdrive.GDriveActivity import net.opendasharchive.openarchive.services.webdav.WebDavActivity +import net.opendasharchive.openarchive.util.Prefs +import net.opendasharchive.openarchive.util.Theme import net.opendasharchive.openarchive.util.extensions.Position import net.opendasharchive.openarchive.util.extensions.getVersionName import net.opendasharchive.openarchive.util.extensions.openBrowser import net.opendasharchive.openarchive.util.extensions.scaled import net.opendasharchive.openarchive.util.extensions.setDrawable import net.opendasharchive.openarchive.util.extensions.styleAsLink +import org.koin.android.ext.android.inject import kotlin.math.roundToInt -class SettingsFragment : Fragment() { +class SettingsFragment : PreferenceFragmentCompat() { - private lateinit var mBinding: FragmentSettingsBinding + private val passcodeRepository by inject() - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { + private var passcodePreference: SwitchPreferenceCompat? = null - mBinding = FragmentSettingsBinding.inflate(inflater, container, false) + private val activityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val passcodeEnabled = result.data?.getBooleanExtra("passcode_enabled", false) ?: false + passcodePreference?.isChecked = passcodeEnabled + } else { + passcodePreference?.isChecked = false + } + } +// override fun onCreateView( +// inflater: LayoutInflater, +// container: ViewGroup?, +// savedInstanceState: Bundle? +// ): View? { +// return ComposeView(requireContext()).apply { +// // Dispose of the Composition when the view's LifecycleOwner +// // is destroyed +// setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) +// setContent { +// Theme { +// SettingsScreen() +// } +// } +// } +// } + + override fun onCreatePreferences( + savedInstanceState: Bundle?, + rootKey: String? + ) { + setPreferencesFromResource(R.xml.prefs_general, rootKey) + + + passcodePreference = findPreference(Prefs.PASSCODE_ENABLED) + + passcodePreference?.setOnPreferenceChangeListener { _, newValue -> + val enabled = newValue as Boolean + if (enabled) { + // Launch PasscodeSetupActivity + val intent = Intent(context, PasscodeSetupActivity::class.java) + activityResultLauncher.launch(intent) + } else { + // Show confirmation dialog + AlertDialog.Builder(requireContext()) + .setTitle("Disable Passcode") + .setMessage("Are you sure you want to disable the passcode?") + .setPositiveButton("Yes") { _, _ -> + passcodeRepository.clearPasscode() + passcodePreference?.isChecked = false + + // Update the FLAG_SECURE dynamically + (activity as? BaseActivity)?.updateScreenshotPrevention() + } + .setNegativeButton("No") { _, _ -> + passcodePreference?.isChecked = true + } + .show() + } + // Return false to avoid the preference updating immediately + false + } - mBinding.btGeneral.setDrawable(R.drawable.ic_account_circle, Position.Start, 0.6) - mBinding.btGeneral.compoundDrawablePadding = - resources.getDimension(R.dimen.padding_small).roundToInt() - mBinding.btGeneral.setOnClickListener { - val context = context ?: return@setOnClickListener + findPreference(Prefs.PROHIBIT_SCREENSHOTS)?.setOnPreferenceClickListener { _ -> + if (activity is BaseActivity) { + // make sure this gets settings change gets applied instantly + // (all other activities rely on the hook in BaseActivity.onResume()) + (activity as BaseActivity).updateScreenshotPrevention() + } - startActivity(Intent(context, GeneralSettingsActivity::class.java)) + true } - mBinding.btSpace.compoundDrawablePadding = - resources.getDimension(R.dimen.padding_small).roundToInt() - mBinding.btSpace.setOnClickListener { - startSpaceAuthActivity() + getPrefByKey(R.string.pref_media_servers)?.setOnPreferenceClickListener { + startActivity(Intent(context, SpacesActivity::class.java)) + true } - mBinding.btFolders.setDrawable(R.drawable.ic_folder, Position.Start, 0.6) - mBinding.btFolders.compoundDrawablePadding = - resources.getDimension(R.dimen.padding_small).roundToInt() - mBinding.btFolders.setOnClickListener { - val context = context ?: return@setOnClickListener - + getPrefByKey(R.string.pref_media_folders)?.setOnPreferenceClickListener { startActivity(Intent(context, FoldersActivity::class.java)) + true } - mBinding.btAbout.text = getString(R.string.action_about, getString(R.string.app_name)) - mBinding.btAbout.styleAsLink() - mBinding.btAbout.setOnClickListener { - context?.openBrowser("https://open-archive.org/save") + findPreference("proof_mode")?.setOnPreferenceClickListener { + startActivity(Intent(context, ProofModeSettingsActivity::class.java)) + true } - mBinding.btPrivacy.styleAsLink() - mBinding.btPrivacy.setOnClickListener { - context?.openBrowser("https://open-archive.org/privacy") + findPreference(Prefs.USE_TOR)?.setOnPreferenceChangeListener { _, newValue -> + //Prefs.useTor = (newValue as Boolean) + //torViewModel.updateTorServiceState() + false } - val activity = activity + findPreference(Prefs.THEME)?.setOnPreferenceChangeListener { _, newValue -> + Theme.set(Theme.get(newValue as? String)) - if (activity != null) { - mBinding.version.text = getString( - R.string.version__, - activity.packageManager.getVersionName(activity.packageName) - ) + true } - return mBinding.root - } + findPreference(Prefs.UPLOAD_WIFI_ONLY)?.setOnPreferenceChangeListener { _, newValue -> + val intent = + Intent(Prefs.UPLOAD_WIFI_ONLY).apply { putExtra("value", newValue as Boolean) } + // Replace with shared ViewModel + LiveData + // LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent) + true + } - override fun onResume() { - super.onResume() + val packageManager = requireActivity().packageManager + val versionText = packageManager.getVersionName(requireActivity().packageName) - updateSpace() + findPreference("app_version")?.summary = versionText } - private fun updateSpace() { - val context = context ?: return - val space = Space.current - - if (space != null) { - mBinding.btSpace.text = space.friendlyName - - mBinding.btSpace.setDrawable( - space.getAvatar(context)?.scaled(24, context), - Position.Start, tint = true - ) - } else { - mBinding.btSpace.visibility = View.GONE - } + private fun getPrefByKey(key: Int): T? { + return findPreference(getString(key)) } - private fun startSpaceAuthActivity() { - val space = Space.current ?: return + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - val clazz = when (space.tType) { - Space.Type.INTERNET_ARCHIVE -> InternetArchiveActivity::class.java - Space.Type.GDRIVE -> GDriveActivity::class.java - else -> WebDavActivity::class.java - } - - val intent = Intent(context, clazz) - intent.putExtra(BaseActivity.EXTRA_DATA_SPACE, space.id) - - startActivity(intent) + view.setPadding(0, 16.dpToPx(), 0, 0) } + + fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt() + + +// mBinding.btAbout.text = getString(R.string.action_about, getString(R.string.app_name)) +// mBinding.btAbout.styleAsLink() +// mBinding.btAbout.setOnClickListener { +// context?.openBrowser("https://open-archive.org/save") +// } +// +// mBinding.btPrivacy.styleAsLink() +// mBinding.btPrivacy.setOnClickListener { +// context?.openBrowser("https://open-archive.org/privacy") +// } +// +// val activity = activity +// +// if (activity != null) { +// mBinding.version.text = getString( +// R.string.version__, +// activity.packageManager.getVersionName(activity.packageName) +// ) +// } } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsScreen.kt new file mode 100644 index 00000000..d612e334 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsScreen.kt @@ -0,0 +1,124 @@ +package net.opendasharchive.openarchive.features.settings + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import me.zhanghai.compose.preference.ProvidePreferenceLocals +import me.zhanghai.compose.preference.listPreference +import me.zhanghai.compose.preference.preference +import me.zhanghai.compose.preference.preferenceCategory +import me.zhanghai.compose.preference.switchPreference +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.DefaultScaffoldPreview + +@Composable +fun SettingsScreen() { + + val context = LocalContext.current + + ProvidePreferenceLocals { + LazyColumn(modifier = Modifier.fillMaxSize()) { + // Secure Category + preferenceCategory(title = { Text("Secure") }, key = "secure") + + switchPreference( + key = "pref_app_passcode", + defaultValue = false, + title = { Text("Lock app with passcode") }, + summary = { Text("6 digit passcode") }) + + // Archive Category + preferenceCategory(title = { Text("Archive") }, key = "archive") + preference( + key = "pref_media_servers", + title = { Text("Media Servers") }, + summary = { Text("Add or remove media servers") }) + preference( + key = "pref_media_folders", + title = { Text("Media Folders") }, + summary = { Text("Add or remove media folders") }) + + // Verify Category + preferenceCategory(title = { Text("Verify") }, key = "verify") + preference( + key = "proof_mode", title = { Text("Proof Mode") }) + + // Encrypt Category + preferenceCategory(title = { Text("Encrypt") }, key = "encrypt") + switchPreference( + key = "use_tor", + defaultValue = false, + title = { Text("Use Tor") }, + summary = { Text("Enable Tor for encryption") }) + + // General Category + preferenceCategory(title = { Text("General") }, key = "general") + switchPreference( + key = "upload_wifi_only", + defaultValue = false, + title = { Text("Upload over Wi-Fi only") }, + summary = { Text("Only upload media when connected to Wi-Fi") }) + listPreference( + key = "theme", + title = { Text("Theme") }, + summary = { Text("Choose app theme") }, + values = listOf( + "light" to "Light", "dark" to "Dark", "system" to "System Default" + ), + defaultValue = "system" + ) + + // About Category + preferenceCategory(title = { Text("About") }, key = "about") + preference( + key = "about_app", + title = { Text("Save by Open Archive") }, + summary = { Text("Tap to view about Save App") }, + onClick = { + // Handle URL intent + openUrl(context, "https://open-archive.org/save") + }) + preference( + key = "privacy_policy", + title = { Text("Terms & Privacy Policy") }, + summary = { Text("Tap to view our Terms & Privacy Policy") }, + onClick = { + // Handle URL intent + openUrl(context, "https://open-archive.org/privacy") + }) + preference( + key = "app_version", + title = { Text("Version") }, + summary = { Text("0.7.2.4783") }, + enabled = false + ) + } + } +} + +// Helper function for opening URLs + +private fun openUrl(context: Context, url: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) +} + + +@Preview +@Composable +private fun SettingsScreenPreview() { + DefaultScaffoldPreview { + + SettingsScreen() + } +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt index c253ed62..5640b8a8 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt @@ -6,14 +6,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import net.opendasharchive.openarchive.databinding.FragmentSpaceSetupBinding import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.main.MainActivity +import net.opendasharchive.openarchive.features.onboarding.BaseFragment import net.opendasharchive.openarchive.util.extensions.hide -class SpaceSetupFragment : Fragment() { +class SpaceSetupFragment : BaseFragment() { private lateinit var mBinding: FragmentSpaceSetupBinding @@ -74,4 +74,8 @@ class SpaceSetupFragment : Fragment() { const val RESULT_VAL_INTERNET_ARCHIVE = "internet_archive" const val RESULT_VAL_GDRIVE = "gdrive" } + + override fun getToolbarTitle() = "Select a Server" + override fun getToolbarSubtitle(): String? = null + override fun shouldShowBackButton() = true } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupSuccessFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupSuccessFragment.kt index 0a77766e..a613cdaa 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupSuccessFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupSuccessFragment.kt @@ -5,11 +5,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import net.opendasharchive.openarchive.databinding.FragmentSpaceSetupSuccessBinding +import net.opendasharchive.openarchive.features.onboarding.BaseFragment -class SpaceSetupSuccessFragment : Fragment() { +class SpaceSetupSuccessFragment : BaseFragment() { private lateinit var mBinding: FragmentSpaceSetupSuccessBinding private var message = "" @@ -50,4 +50,7 @@ class SpaceSetupSuccessFragment : Fragment() { } } } + + override fun getToolbarTitle() = "Setup complete" + override fun shouldShowBackButton() = false } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpacesActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpacesActivity.kt new file mode 100644 index 00000000..0e4f8ab9 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpacesActivity.kt @@ -0,0 +1,81 @@ +package net.opendasharchive.openarchive.features.settings + +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import androidx.recyclerview.widget.LinearLayoutManager +import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.SpaceAdapter +import net.opendasharchive.openarchive.SpaceAdapterListener +import net.opendasharchive.openarchive.SpaceItemDecoration +import net.opendasharchive.openarchive.databinding.ActivitySpacesBinding +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.core.BaseActivity +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveActivity +import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity +import net.opendasharchive.openarchive.services.gdrive.GDriveActivity +import net.opendasharchive.openarchive.services.webdav.WebDavActivity + +class SpacesActivity : BaseActivity(), SpaceAdapterListener { + + private lateinit var mBinding: ActivitySpacesBinding + private lateinit var mAdapter: SpaceAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + + mBinding = ActivitySpacesBinding.inflate(layoutInflater) + setContentView(mBinding.root) + + setupToolbar(title = "Servers", showBackButton = true) + + mAdapter = SpaceAdapter(context = this, listener = this) + + mBinding.rvProjects.layoutManager = LinearLayoutManager(this) + val spacing = resources.getDimensionPixelSize(R.dimen.list_item_spacing) + mBinding.rvProjects.addItemDecoration(SpaceItemDecoration(spacing)) + mBinding.rvProjects.adapter = mAdapter + + + mBinding.fabAdd.setOnClickListener { + startActivity(Intent(this, SpaceSetupActivity::class.java)) + } + } + + override fun onResume() { + super.onResume() + + val projects = Space.getAll().asSequence().toList() + + mAdapter.update(projects) + } + + override fun spaceClicked(space: Space) { + Space.current = space + finish() + } + + override fun editSpaceClicked(spaceId: Long?) { + startSpaceAuthActivity(spaceId) + } + + override fun getSelectedSpace(): Space? { + return Space.current + } + + private fun startSpaceAuthActivity(spaceId: Long?) { + val space = Space.get(spaceId ?: return) ?: return + + val clazz = when (space.tType) { + Space.Type.INTERNET_ARCHIVE -> InternetArchiveActivity::class.java + Space.Type.GDRIVE -> GDriveActivity::class.java + else -> WebDavActivity::class.java + } + + val intent = Intent(this@SpacesActivity, clazz) + intent.putExtra(EXTRA_DATA_SPACE, space.id) + + startActivity(intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/NumericKeypad.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/NumericKeypad.kt index 6186a99a..57751fee 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/NumericKeypad.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/NumericKeypad.kt @@ -148,7 +148,7 @@ private fun NumberButton( onClick() } ) - .border(width = 2.dp, color = MaterialTheme.colorScheme.primary, shape = CircleShape) + .border(width = 2.dp, color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f), shape = CircleShape) .size(72.dp), contentAlignment = Alignment.Center ) { diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/PasscodeDots.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/PasscodeDots.kt index a23886ba..a3596aa5 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/PasscodeDots.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/PasscodeDots.kt @@ -1,5 +1,6 @@ package net.opendasharchive.openarchive.features.settings.passcode.components +import android.content.res.Configuration import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.foundation.background @@ -15,8 +16,10 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.DefaultScaffoldPreview import kotlin.math.roundToInt @Composable @@ -60,11 +63,24 @@ fun PasscodeDots( modifier = Modifier .size(20.dp) .background( - color = if (index < currentPasscodeLength) MaterialTheme.colorScheme.primary + color = if (index < currentPasscodeLength) MaterialTheme.colorScheme.onBackground else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), shape = CircleShape ) ) } } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview +@Composable +private fun PasswordDotsPreview() { + + DefaultScaffoldPreview { + PasscodeDots( + passcodeLength = 6, + currentPasscodeLength = 3 + ) + } } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryScreen.kt index 9eecbed5..28a8eb23 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryScreen.kt @@ -32,6 +32,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.collectLatest import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.presentation.theme.Theme +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.DefaultScaffoldPreview import net.opendasharchive.openarchive.features.settings.passcode.AppHapticFeedbackType import net.opendasharchive.openarchive.features.settings.passcode.HapticManager import net.opendasharchive.openarchive.features.settings.passcode.components.MessageManager @@ -179,6 +180,7 @@ fun PasscodeEntryScreenContent( style = TextStyle( fontSize = 16.sp, fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground ), ) } @@ -194,7 +196,8 @@ fun PasscodeEntryScreenContent( modifier = Modifier.padding(8.dp), style = TextStyle( fontSize = 16.sp, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground ), ) } @@ -213,13 +216,15 @@ fun PasscodeEntryScreenContent( @Composable private fun PasscodeEntryScreenPreview() { - Theme { - PasscodeEntryScreenContent( - state = PasscodeEntryScreenState( - passcodeLength = 6 - ), - onAction = {}, - onExit = {}, - ) + DefaultScaffoldPreview { + Theme { + PasscodeEntryScreenContent( + state = PasscodeEntryScreenState( + passcodeLength = 6 + ), + onAction = {}, + onExit = {}, + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupScreen.kt index e9941b15..b1405697 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupScreen.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.collectLatest import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.core.presentation.theme.Theme +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.DefaultScaffoldPreview import net.opendasharchive.openarchive.features.settings.passcode.AppHapticFeedbackType import net.opendasharchive.openarchive.features.settings.passcode.HapticManager import net.opendasharchive.openarchive.features.settings.passcode.components.MessageManager @@ -207,7 +207,7 @@ private fun PasscodeSetupScreenContent( @Preview @Composable private fun PasscodeSetupScreenPreview() { - Theme { + DefaultScaffoldPreview { PasscodeSetupScreenContent( state = PasscodeSetupUiState( passcodeLength = 6 diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveActivity.kt index e8195e35..592cda97 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveActivity.kt @@ -30,9 +30,7 @@ class GDriveActivity : BaseActivity() { if (space != null) removeSpace(space) } - setSupportActionBar(mBinding.toolbar) - supportActionBar?.title = getString(R.string.gdrive) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + setupToolbar(getString(R.string.gdrive)) mBinding.gdriveId.setText(space?.displayname ?: "") } diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveConduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveConduit.kt index 2420e134..a28d025f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveConduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveConduit.kt @@ -20,7 +20,7 @@ import com.google.api.services.drive.model.File import info.guardianproject.netcipher.proxy.OrbotHelper import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.db.Media -import net.opendasharchive.openarchive.features.folders.BrowseFoldersViewModel +import net.opendasharchive.openarchive.features.folders.Folder import net.opendasharchive.openarchive.services.Conduit import net.opendasharchive.openarchive.util.Prefs import org.apache.http.conn.ClientConnectionManager @@ -174,8 +174,8 @@ class GDriveConduit(media: Media, context: Context) : Conduit(media, context) { return parentFolder } - fun listFoldersInRoot(gdrive: Drive): List { - val result = ArrayList() + fun listFoldersInRoot(gdrive: Drive): List { + val result = ArrayList() try { var pageToken: String? = null do { @@ -185,7 +185,7 @@ class GDriveConduit(media: Media, context: Context) : Conduit(media, context) { .setFields("nextPageToken, files(id, name, createdTime)").execute() for (f in folders.files) { val date = Date(f.createdTime.value) - result.add(BrowseFoldersViewModel.Folder(f.name, date)) + result.add(Folder(f.name, date)) } pageToken = folders.nextPageToken } while (pageToken != null) diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveFragment.kt index 4598d2fe..0c58fbd6 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveFragment.kt @@ -9,7 +9,6 @@ import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.core.text.HtmlCompat -import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import com.google.android.gms.auth.api.Auth import com.google.android.gms.auth.api.signin.GoogleSignIn @@ -17,12 +16,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch -import net.opendasharchive.openarchive.CleanInsightsManager import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.databinding.FragmentGdriveBinding import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.onboarding.BaseFragment -class GDriveFragment : Fragment() { +class GDriveFragment : BaseFragment() { private lateinit var mBinding: FragmentGdriveBinding @@ -150,4 +149,6 @@ class GDriveFragment : Fragment() { mBinding.btAuthenticate.isEnabled = true } } + + override fun getToolbarTitle() = "Google Drive" } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavActivity.kt index b3959cb6..a6bd2af7 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavActivity.kt @@ -26,8 +26,7 @@ class WebDavActivity : BaseActivity() { mBinding = ActivityWebdavBinding.inflate(layoutInflater) setContentView(mBinding.root) - setSupportActionBar(mBinding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + setupToolbar(title = "Edit Private Server", showBackButton = true) mSpaceId = intent.getLongExtra(EXTRA_DATA_SPACE, WebDavFragment.ARG_VAL_NEW_SPACE) diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt index 27737ed4..b935a9ab 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt @@ -9,7 +9,6 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.CoroutineScope @@ -19,6 +18,7 @@ import net.opendasharchive.openarchive.BuildConfig import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.databinding.FragmentWebDavBinding import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.onboarding.BaseFragment import net.opendasharchive.openarchive.services.SaveClient import net.opendasharchive.openarchive.services.internetarchive.Util import net.opendasharchive.openarchive.util.AlertHelper @@ -31,7 +31,7 @@ import okhttp3.Response import java.io.IOException import kotlin.coroutines.suspendCoroutine -class WebDavFragment : Fragment() { +class WebDavFragment : BaseFragment() { private var mSpaceId: Long? = null private lateinit var mSpace: Space @@ -51,7 +51,7 @@ class WebDavFragment : Fragment() { mSpaceId = arguments?.getLong(ARG_SPACE_ID) ?: ARG_VAL_NEW_SPACE - if (ARG_VAL_NEW_SPACE != mSpaceId) { + if (mSpaceId != ARG_VAL_NEW_SPACE) { // setup views for editing and existing space mSpace = Space.get(mSpaceId!!) ?: Space(Space.Type.WEBDAV) @@ -80,7 +80,6 @@ class WebDavFragment : Fragment() { // } - binding.btRemove.setOnClickListener { removeProject() } @@ -154,12 +153,6 @@ class WebDavFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) mSnackbar = binding.root.makeSnackBar(getString(R.string.login_activity_logging_message)) - - if (BuildConfig.DEBUG) { - binding.server.setText("https://nx27277.your-storageshare.de/") - binding.username.setText("Upul") - binding.password.setText("J7wc(ka_4#9!13h&") - } } private fun fixSpaceUrl(url: CharSequence?): Uri? { @@ -357,4 +350,10 @@ class WebDavFragment : Fragment() { @JvmStatic fun newInstance() = newInstance(ARG_VAL_NEW_SPACE) } + + override fun getToolbarTitle(): String = if (mSpaceId == ARG_VAL_NEW_SPACE) { + "Add Private Server" + } else { + "Edit Private Server" + } } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavSetupLicenseFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavSetupLicenseFragment.kt index 75337de8..d1804df1 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavSetupLicenseFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavSetupLicenseFragment.kt @@ -7,15 +7,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.databinding.FragmentWebdavSetupLicenseBinding import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.onboarding.BaseFragment import net.opendasharchive.openarchive.features.settings.CcSelector import kotlin.properties.Delegates -class WebDavSetupLicenseFragment: Fragment() { +class WebDavSetupLicenseFragment: BaseFragment() { private lateinit var binding: FragmentWebdavSetupLicenseBinding @@ -108,4 +108,8 @@ class WebDavSetupLicenseFragment: Fragment() { } } } + + override fun getToolbarTitle() = "Select a License" + override fun getToolbarSubtitle(): String? = null + override fun shouldShowBackButton() = false } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerActivity.kt index bc464488..493a1ce1 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerActivity.kt @@ -59,8 +59,10 @@ class UploadManagerActivity : BaseActivity() { mBinding = ActivityUploadManagerBinding.inflate(layoutInflater) setContentView(mBinding.root) - setSupportActionBar(mBinding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + setupToolbar( + title = getString(R.string.uploads), + showBackButton = true + ) mFrag = supportFragmentManager.findFragmentById(R.id.fragUploadManager) as? UploadManagerFragment } diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/Prefs.kt b/app/src/main/java/net/opendasharchive/openarchive/util/Prefs.kt index 67f55697..23e90f4b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/util/Prefs.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/util/Prefs.kt @@ -11,10 +11,10 @@ import org.witness.proofmode.ProofModeConstants object Prefs { const val PASSCODE_ENABLED = "passcode_enabled" private const val DID_COMPLETE_ONBOARDING = "did_complete_onboarding" - private const val UPLOAD_WIFI_ONLY = "upload_wifi_only" + const val UPLOAD_WIFI_ONLY = "upload_wifi_only" private const val NEARBY_USE_BLUETOOTH = "nearby_use_bluetooth" private const val NEARBY_USE_WIFI = "nearby_use_wifi" - private const val USE_TOR = "use_tor" + const val USE_TOR = "use_tor" const val PROHIBIT_SCREENSHOTS = "prohibit_screenshots" const val USE_PROOFMODE = "use_proofmode" const val USE_PROOFMODE_KEY_ENCRYPTION = "proofmode_key_encryption" @@ -23,6 +23,7 @@ object Prefs { private const val CURRENT_SPACE_ID = "current_space" private const val FLAG_HINT_SHOWN = "ft.flag" private const val BATCH_HINT_SHOWN = "ft.batch" + private const val ADD_MEDIA_HINT = "ft.addMedia" private const val DONT_SHOW_UPLOAD_HINT = "ft.upload" private const val IA_HINT_SHOWN = "ft.ia" private const val ADD_FOLDER_HINT_SHOWN = "ft.add_folder" @@ -120,6 +121,12 @@ object Prefs { prefs?.edit()?.putBoolean(FLAG_HINT_SHOWN, value)?.apply() } + var addMediaHint: Boolean + get() = prefs?.getBoolean(ADD_MEDIA_HINT, false) ?: false + set(value) { + prefs?.edit()?.putBoolean(ADD_MEDIA_HINT, value)?.apply() + } + var batchHintShown: Boolean get() = prefs?.getBoolean(BATCH_HINT_SHOWN, false) ?: false set(value) { diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/Theme.kt b/app/src/main/java/net/opendasharchive/openarchive/util/Theme.kt index 5099d1cb..ea053a39 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/util/Theme.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/util/Theme.kt @@ -45,7 +45,7 @@ enum class Theme(val mode: Int) { * @param name: Theme name, case insensitive. */ fun get(name: String?): Theme { - return values().firstOrNull { it.name.uppercase() == name?.uppercase() } ?: SYSTEM + return entries.firstOrNull { it.name.uppercase() == name?.uppercase() } ?: SYSTEM } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/extensions/TextView.kt b/app/src/main/java/net/opendasharchive/openarchive/util/extensions/TextView.kt index 98d3b429..f5f9ece8 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/util/extensions/TextView.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/util/extensions/TextView.kt @@ -44,10 +44,10 @@ fun TextView.setDrawable(drawable: Drawable?, position: Position, scale: Double } fun TextView.styleAsLink() { - setText(SpannableString(text).apply { - setSpan(URLSpan(""), 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - }, TextView.BufferType.SPANNABLE) + setText(SpannableString(text).apply { + setSpan(URLSpan(""), 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + }, TextView.BufferType.SPANNABLE) - isClickable = true - isFocusable = true + isClickable = true + isFocusable = true } \ No newline at end of file diff --git a/app/src/main/res/animator/custom_fab_animator.xml b/app/src/main/res/animator/custom_fab_animator.xml new file mode 100644 index 00000000..6c2fe41c --- /dev/null +++ b/app/src/main/res/animator/custom_fab_animator.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/hands_mobile_updated.xml b/app/src/main/res/drawable-night/hands_mobile_updated.xml new file mode 100644 index 00000000..f934e7d2 --- /dev/null +++ b/app/src/main/res/drawable-night/hands_mobile_updated.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/avd_splash_animator.xml b/app/src/main/res/drawable/avd_splash_animator.xml new file mode 100644 index 00000000..60d759ba --- /dev/null +++ b/app/src/main/res/drawable/avd_splash_animator.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/baseline_add_24.xml b/app/src/main/res/drawable/baseline_add_24.xml new file mode 100644 index 00000000..9f83b8fb --- /dev/null +++ b/app/src/main/res/drawable/baseline_add_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/bottom_nav_background.xml b/app/src/main/res/drawable/bottom_nav_background.xml index a9012069..9d289262 100644 --- a/app/src/main/res/drawable/bottom_nav_background.xml +++ b/app/src/main/res/drawable/bottom_nav_background.xml @@ -1,5 +1,6 @@ + diff --git a/app/src/main/res/drawable/button.xml b/app/src/main/res/drawable/button.xml index ff4bc721..1cdaaf9f 100644 --- a/app/src/main/res/drawable/button.xml +++ b/app/src/main/res/drawable/button.xml @@ -2,5 +2,5 @@ - + diff --git a/app/src/main/res/drawable/gradient_fade.xml b/app/src/main/res/drawable/gradient_fade.xml new file mode 100644 index 00000000..04f68990 --- /dev/null +++ b/app/src/main/res/drawable/gradient_fade.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/hands_mobile_updated.xml b/app/src/main/res/drawable/hands_mobile_updated.xml new file mode 100644 index 00000000..57e261b1 --- /dev/null +++ b/app/src/main/res/drawable/hands_mobile_updated.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml deleted file mode 100644 index 98d63a5c..00000000 --- a/app/src/main/res/drawable/ic_add.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 00000000..075e95dc --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 00000000..3c53db7e --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_folder_add.xml b/app/src/main/res/drawable/ic_folder_add.xml new file mode 100644 index 00000000..ce96535d --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_add.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_folder_search.xml b/app/src/main/res/drawable/ic_folder_search.xml new file mode 100644 index 00000000..54664ed8 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_search.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_folder_selected.xml b/app/src/main/res/drawable/ic_folder_selected.xml new file mode 100644 index 00000000..64c76b60 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_selected.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_folder_unselected.xml b/app/src/main/res/drawable/ic_folder_unselected.xml new file mode 100644 index 00000000..2d3be5e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_unselected.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_image_gallery_line.xml b/app/src/main/res/drawable/ic_image_gallery_line.xml new file mode 100644 index 00000000..000fe2b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_gallery_line.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 00000000..ef3a1feb --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_tick_circle.xml b/app/src/main/res/drawable/ic_tick_circle.xml new file mode 100644 index 00000000..10d7482c --- /dev/null +++ b/app/src/main/res/drawable/ic_tick_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/item_background_selector.xml b/app/src/main/res/drawable/item_background_selector.xml new file mode 100644 index 00000000..65f25966 --- /dev/null +++ b/app/src/main/res/drawable/item_background_selector.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/keyboard_arrow_right.xml b/app/src/main/res/drawable/keyboard_arrow_right.xml new file mode 100644 index 00000000..4904368f --- /dev/null +++ b/app/src/main/res/drawable/keyboard_arrow_right.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/pill_bg.xml b/app/src/main/res/drawable/pill_bg.xml new file mode 100644 index 00000000..d5bbddae --- /dev/null +++ b/app/src/main/res/drawable/pill_bg.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_dialog.xml b/app/src/main/res/drawable/rounded_dialog.xml new file mode 100644 index 00000000..5f6dc817 --- /dev/null +++ b/app/src/main/res/drawable/rounded_dialog.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/splash_branding_image.xml b/app/src/main/res/drawable/splash_branding_image.xml new file mode 100644 index 00000000..bea7ce69 --- /dev/null +++ b/app/src/main/res/drawable/splash_branding_image.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/splash_icon.xml b/app/src/main/res/drawable/splash_icon.xml new file mode 100644 index 00000000..4e2b4ab7 --- /dev/null +++ b/app/src/main/res/drawable/splash_icon.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/font/montserrat.xml b/app/src/main/res/font/montserrat.xml index 560bf61a..60bf7d17 100644 --- a/app/src/main/res/font/montserrat.xml +++ b/app/src/main/res/font/montserrat.xml @@ -1,13 +1,38 @@ - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/montserrat_light.ttf b/app/src/main/res/font/montserrat_light.ttf new file mode 100644 index 00000000..9d894920 Binary files /dev/null and b/app/src/main/res/font/montserrat_light.ttf differ diff --git a/app/src/main/res/font/montserrat_light_italic.ttf b/app/src/main/res/font/montserrat_light_italic.ttf new file mode 100644 index 00000000..bf854d4b Binary files /dev/null and b/app/src/main/res/font/montserrat_light_italic.ttf differ diff --git a/app/src/main/res/font/montserrat_medium.ttf b/app/src/main/res/font/montserrat_medium.ttf new file mode 100644 index 00000000..dfbcfe4f Binary files /dev/null and b/app/src/main/res/font/montserrat_medium.ttf differ diff --git a/app/src/main/res/font/montserrat_medium_italic.ttf b/app/src/main/res/font/montserrat_medium_italic.ttf new file mode 100644 index 00000000..1e674770 Binary files /dev/null and b/app/src/main/res/font/montserrat_medium_italic.ttf differ diff --git a/app/src/main/res/font/montserrat_regular.ttf b/app/src/main/res/font/montserrat_regular.ttf index aa9033a8..48ba65ed 100644 Binary files a/app/src/main/res/font/montserrat_regular.ttf and b/app/src/main/res/font/montserrat_regular.ttf differ diff --git a/app/src/main/res/font/montserrat_semi_bold.ttf b/app/src/main/res/font/montserrat_semi_bold.ttf new file mode 100644 index 00000000..8dbcdb39 Binary files /dev/null and b/app/src/main/res/font/montserrat_semi_bold.ttf differ diff --git a/app/src/main/res/font/montserrat_semi_bold_italic.ttf b/app/src/main/res/font/montserrat_semi_bold_italic.ttf new file mode 100644 index 00000000..8604419a Binary files /dev/null and b/app/src/main/res/font/montserrat_semi_bold_italic.ttf differ diff --git a/app/src/main/res/font/montserrat_thin.ttf b/app/src/main/res/font/montserrat_thin.ttf new file mode 100644 index 00000000..2a85a52c Binary files /dev/null and b/app/src/main/res/font/montserrat_thin.ttf differ diff --git a/app/src/main/res/font/montserrat_thin_italic.ttf b/app/src/main/res/font/montserrat_thin_italic.ttf new file mode 100644 index 00000000..5dc7160a Binary files /dev/null and b/app/src/main/res/font/montserrat_thin_italic.ttf differ diff --git a/app/src/main/res/layout/activity_add_folder.xml b/app/src/main/res/layout/activity_add_folder.xml index 24d35f2c..42438c74 100644 --- a/app/src/main/res/layout/activity_add_folder.xml +++ b/app/src/main/res/layout/activity_add_folder.xml @@ -1,6 +1,5 @@ - - + + - + android:layout_marginBottom="24dp" + android:orientation="vertical" + android:paddingVertical="8dp"> - - - + + + + + + + android:layout_marginVertical="16dp" + android:background="@drawable/item_background_selector" + android:orientation="horizontal" + android:padding="8dp"> - + + + + + + + + + + android:src="@drawable/keyboard_arrow_right" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:tint="@color/colorText" /> - + - + android:layout_marginVertical="16dp" + android:background="@drawable/item_background_selector" + android:orientation="horizontal" + android:padding="8dp"> - + + + android:gravity="center_vertical" + android:orientation="vertical" + android:paddingStart="8dp" + android:paddingEnd="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/browse_folder_arrow_icon" + app:layout_constraintStart_toEndOf="@id/browse_folder_icon" + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + - + diff --git a/app/src/main/res/layout/activity_browse_folders.xml b/app/src/main/res/layout/activity_browse_folders.xml index 95bb4860..512ede21 100644 --- a/app/src/main/res/layout/activity_browse_folders.xml +++ b/app/src/main/res/layout/activity_browse_folders.xml @@ -1,30 +1,42 @@ - - + android:layout_height="match_parent" + android:orientation="vertical"> - + + + + + + android:layout_marginBottom="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_consent.xml b/app/src/main/res/layout/activity_consent.xml index 9bd25020..85fe89cf 100644 --- a/app/src/main/res/layout/activity_consent.xml +++ b/app/src/main/res/layout/activity_consent.xml @@ -5,19 +5,16 @@ android:layout_height="match_parent" android:orientation="vertical"> - + + + diff --git a/app/src/main/res/layout/activity_create_new_folder.xml b/app/src/main/res/layout/activity_create_new_folder.xml index 31f0b15c..dd194353 100644 --- a/app/src/main/res/layout/activity_create_new_folder.xml +++ b/app/src/main/res/layout/activity_create_new_folder.xml @@ -5,27 +5,24 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:filterTouchesWhenObscured="true" + tools:background="@color/colorBackground" tools:context="net.opendasharchive.openarchive.features.folders.CreateNewFolderActivity"> - + + app:layout_constraintTop_toBottomOf="@id/app_bar_layout" /> diff --git a/app/src/main/res/layout/activity_edit_folder.xml b/app/src/main/res/layout/activity_edit_folder.xml index 7a1189f6..36ff7d2a 100644 --- a/app/src/main/res/layout/activity_edit_folder.xml +++ b/app/src/main/res/layout/activity_edit_folder.xml @@ -6,28 +6,26 @@ android:layout_height="match_parent" android:filterTouchesWhenObscured="true" android:orientation="vertical" + tools:background="@color/colorBackground" tools:context=".features.settings.EditFolderActivity"> - + + + + app:layout_constraintTop_toBottomOf="@id/common_app_bar" /> - + @@ -70,29 +68,44 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/activity_vertical_margin" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toBottomOf="parent"> + app:layout_constraintStart_toStartOf="parent"> - - -