From 345ce007c1b94b2fca9a238b3603fe61257a0056 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 20 Feb 2024 09:42:34 -0800 Subject: [PATCH 01/63] fix(network config): restrict clear text by default Required since the minimum SDK is less than 27 where it became the default --- app/src/main/AndroidManifest.xml | 1 + app/src/main/res/xml/network_security_config.xml | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 21a7fbf4..ac3552ee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -47,6 +47,7 @@ android:label="@string/app_name" android:requestLegacyExternalStorage="true" android:supportsRtl="true" + android:networkSecurityConfig="@xml/network_security_config" android:theme="@style/AppTheme.NoActionBar" tools:replace="android:icon,android:allowBackup" tools:ignore="UnusedAttribute"> diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..1edbb5f5 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From 585b680d559cf21639b1d46cef3e3e22b0b5cb02 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 20 Feb 2024 09:45:03 -0800 Subject: [PATCH 02/63] Revert "fix(network config): restrict clear text by default" This reverts commit 345ce007c1b94b2fca9a238b3603fe61257a0056. Meant to make a PR before merging to development. --- app/src/main/AndroidManifest.xml | 1 - app/src/main/res/xml/network_security_config.xml | 5 ----- 2 files changed, 6 deletions(-) delete mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ac3552ee..21a7fbf4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -47,7 +47,6 @@ android:label="@string/app_name" android:requestLegacyExternalStorage="true" android:supportsRtl="true" - android:networkSecurityConfig="@xml/network_security_config" android:theme="@style/AppTheme.NoActionBar" tools:replace="android:icon,android:allowBackup" tools:ignore="UnusedAttribute"> diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml deleted file mode 100644 index 1edbb5f5..00000000 --- a/app/src/main/res/xml/network_security_config.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file From 8834910cec87b8d01db102ba0898c239fbfe52da Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 20 Feb 2024 09:42:34 -0800 Subject: [PATCH 03/63] fix(network config): restrict clear text by default Required since the minimum SDK is less than 27 where it became the default --- app/src/main/AndroidManifest.xml | 1 + app/src/main/res/xml/network_security_config.xml | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 21a7fbf4..ac3552ee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -47,6 +47,7 @@ android:label="@string/app_name" android:requestLegacyExternalStorage="true" android:supportsRtl="true" + android:networkSecurityConfig="@xml/network_security_config" android:theme="@style/AppTheme.NoActionBar" tools:replace="android:icon,android:allowBackup" tools:ignore="UnusedAttribute"> diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..1edbb5f5 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From ffb62ab396b13d2e5287b1e2b6fdfc217635a700 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 20 Feb 2024 10:35:57 -0800 Subject: [PATCH 04/63] fix(ci): just make circle ci pass for now --- .circleci/config.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d3b31ac9..d9867073 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,16 +3,16 @@ jobs: build: working_directory: ~/code docker: - - image: circleci/android:api-30 + - image: cimg/android:2024.01 environment: JVM_OPTS: -Xmx3200m steps: - checkout - restore_cache: key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} -# - run: -# name: Chmod permissions #if permission for Gradlew Dependencies fail, use this. -# command: sudo chmod +x ./gradlew + # - run: + # name: Chmod permissions #if permission for Gradlew Dependencies fail, use this. + # command: sudo chmod +x ./gradlew - run: name: Create local.properties command: touch local.properties @@ -26,10 +26,9 @@ jobs: - run: name: Run Tests command: ./gradlew lint test - - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ path: app/build/reports destination: reports - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ path: app/build/test-results # See https://circleci.com/docs/2.0/deployment-integrations/ for deploy examples - From 0a40b7e08c278097759fe2e7c5f49e2afaa1cd2b Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 20 Feb 2024 11:26:52 -0800 Subject: [PATCH 05/63] fix(tor): ensure registered recievers are not exported in android 14+ make use of compatibility helpers --- .../openarchive/upload/BroadcastManager.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt index 7d3ec687..3cd2d11e 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt @@ -2,8 +2,11 @@ package net.opendasharchive.openarchive.upload import android.content.BroadcastReceiver import android.content.Context +import android.content.Context.RECEIVER_NOT_EXPORTED import android.content.Intent import android.content.IntentFilter +import android.os.Build +import androidx.core.content.ContextCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager object BroadcastManager { @@ -37,11 +40,10 @@ object BroadcastManager { } fun register(context: Context, receiver: BroadcastReceiver) { - LocalBroadcastManager.getInstance(context) - .registerReceiver(receiver, IntentFilter(Action.Change.id)) - - LocalBroadcastManager.getInstance(context) - .registerReceiver(receiver, IntentFilter(Action.Delete.id)) + LocalBroadcastManager.getInstance(context).apply { + ContextCompat.registerReceiver(context, receiver, IntentFilter(Action.Change.id), ContextCompat.RECEIVER_NOT_EXPORTED) + ContextCompat.registerReceiver(context, receiver, IntentFilter(Action.Delete.id), ContextCompat.RECEIVER_NOT_EXPORTED) + } } fun unregister(context: Context, receiver: BroadcastReceiver) { From 21280c7ebba1f343ab8c3869585a9e459d4943a1 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Fri, 23 Feb 2024 11:12:46 -0800 Subject: [PATCH 06/63] fix(localization): typo in google drive string --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aeb60700..62bc0c64 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,7 +23,7 @@ Google Drive Google Drive™ Upload to Google Drive - Sing in or create an account with Google Drive + Sign in or create an account with Google Drive From 929a123c530746ab66e784e78d3a0becef28edc3 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Fri, 23 Feb 2024 11:39:19 -0800 Subject: [PATCH 07/63] fix(upload): use generic error message --- app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt | 1 - .../java/net/opendasharchive/openarchive/db/MediaAdapter.kt | 4 +++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt index d0b167d3..91196213 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt @@ -14,7 +14,6 @@ class SaveApp : SugarApp() { override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) - } override fun onCreate() { diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/MediaAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/db/MediaAdapter.kt index ed43a413..48e42a16 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/db/MediaAdapter.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/db/MediaAdapter.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.view.* import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar +import net.opendasharchive.openarchive.CleanInsightsManager import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.features.media.PreviewActivity import net.opendasharchive.openarchive.upload.BroadcastManager @@ -69,9 +70,10 @@ class MediaAdapter( Media.Status.Error -> { if (supportedStatuses.contains(Media.Status.Error)) { + //CleanInsightsManager.measureEvent("backend", "upload-error", media[pos].space?.friendlyName) mActivity.get()?.let { AlertHelper.show( - it, media[pos].statusMessage, + it, it.getString(R.string.upload_unsuccessful_description), R.string.upload_unsuccessful, R.drawable.ic_error, listOf( AlertHelper.positiveButton(R.string.retry) { _, _ -> media[pos].sStatus = Media.Status.Queued diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aeb60700..57e2d120 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -254,6 +254,7 @@ Step 3: Generate your API keys by selecting the box and it will auto load in the app! Upload Unsuccessful + Unable to upload due to session error, please try again or contact support. Retry Edit Media From 9db86e727d27cf72bd7725c8c3dd0b7a8f94147f Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Fri, 23 Feb 2024 15:08:46 -0800 Subject: [PATCH 08/63] chore(project): update issue templates and fastlane Bugs: include preconditions, actual behaviour, and more environment details Features: add alternate template for user story format Fastlane: use local ENV vars, remove slack and prepare for alpha/beta/internal uploads --- .github/ISSUE_TEMPLATE/bug_report.md | 30 ++++-- .github/ISSUE_TEMPLATE/feature_request.md | 60 ++++++++++- fastlane/Appfile | 2 +- fastlane/Fastfile | 123 ++++++++-------------- 4 files changed, 122 insertions(+), 93 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 849d1dd4..5e46cdcd 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,17 +1,20 @@ --- name: Bug report about: Create a report to document a bug in the app and to help us improve -title: '' +title: "" labels: bug -assignees: '' - +assignees: "" --- **Describe the bug** -A clear and concise description of what the bug is. +A clear and concise description of what the bug is, who it affects, where it happens, and when it occurs. + +**Preconditions** +Include any common configuration, users, or assumptions on how to reproduce. **To Reproduce** -Steps to reproduce the behavior: +Steps to reproduce the behavior. + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -20,13 +23,22 @@ Steps to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. +**Actual behaviour** +Describe the error, flaw, or failure when interacting in more detail. + **Screenshots** If applicable, add screenshots to help explain your problem. +**Logs** +If applicable, include any device logs. + **Environment (please complete the following information):** - - OS version: [e.g. Android 10] - - Device: [e.g. Samsung Galaxy A2] - - App Version [e.g. 2.2] + +- OS version: [e.g. Android 10] +- Device: [e.g. Samsung Galaxy A2] +- App Version [e.g. 2.2] +- Backend(s): [e.g. Internet Archive, Nextcloud] +- Component(s): [e.g. Onboarding, Settings] **Additional context** -Add any other context about the problem here. +Add any other context about the problem here. Please indicate priority and severity. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 5ecce628..10f303df 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +1,70 @@ --- name: Feature request -about: Suggest an idea for this project. Please provide your feedback using the 'I +about: + Suggest an idea for this project. Please provide your feedback using the 'I Like, I Wish, What if' feedback format -title: '' +title: "" labels: enhancement -assignees: '' - +assignees: "" --- -A preview of the [**I Like, I Wish, What if feedback format**](https://public-media.interaction-design.org/pdf/I-Like-I-Wish-What-If.pdf) +A preview of the [**I Like, I Wish, What if feedback format**](https://public-media.interaction-design.org/pdf/I-Like-I-Wish-What-If.pdf) to fascilitate discussions. **I Like...** + **I Wish...** + **What If...** + + +--- + +A preview of the **User Story format** + +## Motivation and Impact + +A clear reasoning of the value of the feature and why it should exist. + +**We believe** +**Will result** +**We can proceed when** + +## User Story (High-Level Acceptance Criteria) + +A description of high-level outcomes for this feature as an end-user. + +As a _(type of user)_, I want _(some goal)_ so that _(some reason)_. + +Given _(precondition)_, when _(action)_, then _(outcome)_. + +## User Flow (optional) + +A step-by-step run-through of the user's journey with screenshots or mockups. + +## Functional Requirements and Developer Notes + +A technical discussion about the requirements with diagrams, schemas, or references. + +## Quality Assurance and Security Notes + +Describe what could go wrong with the system and how it would be handled. + +## Not in Scope, Questions, and Answers + +Mention aspects of the feature that are not in scope. +Ask any remaining questions and answer known issues. + +## Acceptance Criteria + +A detailed list of rules that will consider this feature "done". + +## Affected + +Take note of any people, departments, users, platforms, or systems affected. + +Be sure to tag individuals or representatives. diff --git a/fastlane/Appfile b/fastlane/Appfile index 6c3cd0cf..d13a2606 100644 --- a/fastlane/Appfile +++ b/fastlane/Appfile @@ -1,2 +1,2 @@ -json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one +json_key_file(ENV["FASTLANE_JSON_KEY_FILE"]) # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one package_name("net.opendasharchive.openarchive") # e.g. com.krausefx.app diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7ceb693c..ab92687c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -5,30 +5,32 @@ platform :android do lane :test do gradle(task: "test") end - before_all do - ENV["SLACK_URL"] - end - desc "Submit a new Beta Build to Beta" - lane :beta do + desc "Submit a new Internal Build" + lane :internal do send_progress_message("Build Started :rocket:") gradle(task: "clean assembleRelease") - gradle(task: "assemble", build_type: "debug") - # sh "your_script.sh" - # You can also use other beta testing services here - send_progress_message("Uploading To Slack :rocket:") - upload_to_slack() + send_progress_message("Uploading To Internal track :rocket:") + upload_to_play_store(track: "internal") end - desc "Upload the latest output APK to #distribution Slack channel" - private_lane :upload_to_slack do |options| - file_path = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH] - file_name = file_path.gsub(/\/.*\//,"") - access_token = ENV["ACCESS_TOKEN"] - channel_name = "#distribution" - sh "echo Uploading " + file_name + " to Slack" - sh "curl https://slack.com/api/files.upload -F token=\"" + access_token + "\" -F channels=\"" + channel_name + "\" -F title=\"" + file_name + "\" -F filename=\"" + file_name + "\" -F file=@" + file_path + desc "Submit a new Alpha Build" + lane :alpha do + send_progress_message("Build Started :rocket:") + gradle(task: "clean assembleRelease") + + send_progress_message("Uploading To Alpha track :rocket:") + upload_to_play_store(track: "alpha") + end + + desc "Submit a new Beta Build" + lane :beta do + send_progress_message("Build Started :rocket:") + gradle(task: "clean assembleRelease") + + send_progress_message("Uploading To Beta track :rocket:") + upload_to_play_store(track: "beta") end desc "Deploy a new version to the Google Play" @@ -38,66 +40,31 @@ platform :android do end end - def send_message(message) - slack( - message: message, - success: true, - slack_url: ENV["SLACK_URL"], - attachment_properties: { - fields: [ - { - title: "Build number", - value: ENV["BUILD_NUMBER"], - } - ] - } - ) - end +def send_message(message) + puts message +end - def send_progress_message(message) - slack( - message: message, - success: true, - slack_url: ENV["SLACK_URL"], - ) - end +def send_progress_message(message) + puts message +end - def on_error(exception) - slack( - message: "Some thing goes wrong", - success: false, - slack_url: ENV["SLACK_URL"], - attachment_properties: { - fields: [ - { - title: "Build number", - value: ENV["BUILD_NUMBER"], - }, - { - title: "Error message", - value: exception.to_s, - short: false - } - ] - } - ) +def on_error(exception) + send_message "${exception}" +end - after_all do |lane| - file_name = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH].gsub(/\/.*\//,"") - slack( - message: "Successfully deployed new App Update! :champagne:", - channel: "@user.name,#channel.name", - default_payloads: [ - :git_branch, - :last_git_commit_hash, - :last_git_commit_message - ], - payload: { - # Optional, lets you specify any number of your own Slack attachments. - "Build Date" => Time.new.to_s, - "APK" => file_name - }, - success: true - ) - end -end \ No newline at end of file +after_all do |lane| + file_name = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH].gsub(/\/.*\//,"") + send_message "Successfully deployed new App Update! :champagne:" + default_payloads = [ + :git_branch, + :last_git_commit_hash, + :last_git_commit_message + ] + payload = { + "Build Date" => Time.new.to_s, + "APK" => file_name + } + send_message file_name + send_message "#{default_payloads}" + send_message "#{payload}" +end From e984a72b199bf8bf2b0d55de85f630742e7a51b5 Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Mon, 26 Feb 2024 12:47:33 +0100 Subject: [PATCH 09/63] Updated dependencies. Removed gradle files which only contain version specifications. These thwart Android Studio's update helpers. Added deprecation warnings as per Kotlin update analysis. --- app/build.gradle | 38 +++++++++---------- .../features/main/MainMediaFragment.kt | 1 + .../features/onboarding/SpaceSetupActivity.kt | 1 + .../services/gdrive/GDriveFragment.kt | 1 + build.gradle | 4 +- config.gradle | 9 ----- dependencies.gradle | 6 --- 7 files changed, 22 insertions(+), 38 deletions(-) delete mode 100644 config.gradle delete mode 100644 dependencies.gradle diff --git a/app/build.gradle b/app/build.gradle index b29e07bf..2535cc9a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,5 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply from : '../dependencies.gradle' -apply from : '../config.gradle' apply plugin: 'org.jetbrains.kotlin.android' def getDropboxKey() { @@ -19,14 +17,14 @@ android { signingConfigs { } - compileSdkVersion config.compileSdkVersion - buildToolsVersion config.buildToolsVersion + compileSdkVersion 34 + buildToolsVersion '34.0.0' defaultConfig { applicationId "net.opendasharchive.openarchive" - minSdkVersion config.minSdkVersion - targetSdkVersion config.targetSdkVersion - versionCode config.versionCode - versionName config.versionName + minSdkVersion 21 + targetSdkVersion 34 + versionCode 20549 + versionName '0.3.1-alpha2' archivesBaseName = "Save-$versionName" multiDexEnabled true vectorDrawables.useSupportLibrary = true @@ -75,8 +73,8 @@ android { dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.21" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1" implementation "androidx.core:core-ktx:1.12.0" implementation "androidx.appcompat:appcompat:1.6.1" @@ -84,24 +82,24 @@ dependencies { 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.6.2" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2" + 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-ktx:2.8.1" + implementation "androidx.work:work-runtime-ktx:2.9.0" implementation "com.github.satyan:sugar:1.5" - implementation "com.google.code.gson:gson:2.9.1" - implementation "com.squareup.okhttp3:okhttp:4.10.0" + implementation "com.google.code.gson:gson:2.10" + implementation "com.squareup.okhttp3:okhttp:4.11.0" implementation "com.dropbox.core:dropbox-core-sdk:5.4.4" // adding web dav support: https://github.com/thegrizzlylabs/sardine-android' implementation "com.github.guardianproject:sardine-android:89f7eae512" - implementation "com.google.android.material:material:1.10.0" + implementation "com.google.android.material:material:1.11.0" - implementation "com.github.bumptech.glide:glide:$versions.glide" - annotationProcessor "com.github.bumptech.glide:compiler:$versions.glide" + 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.github.stfalcon:frescoimageviewer:0.5.0" @@ -131,7 +129,7 @@ dependencies { exclude group: "com.squareup.okio", module: "okio" } - implementation "com.google.guava:guava:31.0.1-android" + 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' @@ -144,7 +142,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:5.0.1' // google drive api - implementation 'com.google.android.gms:play-services-auth:20.7.0' + implementation 'com.google.android.gms:play-services-auth:21.0.0' 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') 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 8aaa3f7e..51560572 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 @@ -81,6 +81,7 @@ class MainMediaFragment : Fragment() { BroadcastManager.unregister(requireContext(), mMessageReceiver) } + @Deprecated("Deprecated in Java") override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_delete -> { 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 a691a311..16336697 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 @@ -212,6 +212,7 @@ class SpaceSetupActivity : BaseActivity() { } } + @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) 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 44a22239..9b27ae55 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 @@ -84,6 +84,7 @@ class GDriveFragment : Fragment() { } } + @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) diff --git a/build.gradle b/build.gradle index 08724626..079210f0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,5 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - apply from: './dependencies.gradle' - apply from: './config.gradle' repositories { google() @@ -54,7 +52,7 @@ buildscript { classpath "com.neenbedankt.gradle.plugins:android-apt:1.8" classpath "com.testdroid:gradle:2.63.1" classpath "gradle.plugin.com.browserstack.gradle:browserstack-gradle-plugin:2.3.1" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21" } } diff --git a/config.gradle b/config.gradle deleted file mode 100644 index bdfe3dd9..00000000 --- a/config.gradle +++ /dev/null @@ -1,9 +0,0 @@ -ext.config = [ - buildToolsVersion: '33.0.2', - compileSdkVersion: 34, - minSdkVersion : 21, - targetSdkVersion : 34, - appcompat : 'com.android.support:appcompat-v7:28.0.0', - versionName : '0.3.1-alpha2', - versionCode : 20549 -] diff --git a/dependencies.gradle b/dependencies.gradle deleted file mode 100644 index 786591fb..00000000 --- a/dependencies.gradle +++ /dev/null @@ -1,6 +0,0 @@ -ext { - versions = [ - glide : '4.16.0', - kotlin : '1.8.22', - ] -} \ No newline at end of file From 64eb6bdfca977724e50065b32b182e3c65bd504c Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Fri, 23 Feb 2024 11:12:46 -0800 Subject: [PATCH 10/63] fix(localization): typo in google drive string --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aeb60700..62bc0c64 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,7 +23,7 @@ Google Drive Google Drive™ Upload to Google Drive - Sing in or create an account with Google Drive + Sign in or create an account with Google Drive From 32124f32071fd65d54cad77438e81dabce8928e0 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 20 Feb 2024 11:26:52 -0800 Subject: [PATCH 11/63] fix(tor): ensure registered recievers are not exported in android 14+ make use of compatibility helpers --- .../openarchive/upload/BroadcastManager.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt index 7d3ec687..3cd2d11e 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt @@ -2,8 +2,11 @@ package net.opendasharchive.openarchive.upload import android.content.BroadcastReceiver import android.content.Context +import android.content.Context.RECEIVER_NOT_EXPORTED import android.content.Intent import android.content.IntentFilter +import android.os.Build +import androidx.core.content.ContextCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager object BroadcastManager { @@ -37,11 +40,10 @@ object BroadcastManager { } fun register(context: Context, receiver: BroadcastReceiver) { - LocalBroadcastManager.getInstance(context) - .registerReceiver(receiver, IntentFilter(Action.Change.id)) - - LocalBroadcastManager.getInstance(context) - .registerReceiver(receiver, IntentFilter(Action.Delete.id)) + LocalBroadcastManager.getInstance(context).apply { + ContextCompat.registerReceiver(context, receiver, IntentFilter(Action.Change.id), ContextCompat.RECEIVER_NOT_EXPORTED) + ContextCompat.registerReceiver(context, receiver, IntentFilter(Action.Delete.id), ContextCompat.RECEIVER_NOT_EXPORTED) + } } fun unregister(context: Context, receiver: BroadcastReceiver) { From 09378dd8ac1e3968a71f8891b705efb22eb27d67 Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Mon, 26 Feb 2024 12:47:33 +0100 Subject: [PATCH 12/63] Updated dependencies. Removed gradle files which only contain version specifications. These thwart Android Studio's update helpers. Added deprecation warnings as per Kotlin update analysis. --- app/build.gradle | 38 +++++++++---------- .../features/main/MainMediaFragment.kt | 1 + .../features/onboarding/SpaceSetupActivity.kt | 1 + .../services/gdrive/GDriveFragment.kt | 1 + build.gradle | 4 +- config.gradle | 9 ----- dependencies.gradle | 6 --- 7 files changed, 22 insertions(+), 38 deletions(-) delete mode 100644 config.gradle delete mode 100644 dependencies.gradle diff --git a/app/build.gradle b/app/build.gradle index b29e07bf..2535cc9a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,5 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply from : '../dependencies.gradle' -apply from : '../config.gradle' apply plugin: 'org.jetbrains.kotlin.android' def getDropboxKey() { @@ -19,14 +17,14 @@ android { signingConfigs { } - compileSdkVersion config.compileSdkVersion - buildToolsVersion config.buildToolsVersion + compileSdkVersion 34 + buildToolsVersion '34.0.0' defaultConfig { applicationId "net.opendasharchive.openarchive" - minSdkVersion config.minSdkVersion - targetSdkVersion config.targetSdkVersion - versionCode config.versionCode - versionName config.versionName + minSdkVersion 21 + targetSdkVersion 34 + versionCode 20549 + versionName '0.3.1-alpha2' archivesBaseName = "Save-$versionName" multiDexEnabled true vectorDrawables.useSupportLibrary = true @@ -75,8 +73,8 @@ android { dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.21" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1" implementation "androidx.core:core-ktx:1.12.0" implementation "androidx.appcompat:appcompat:1.6.1" @@ -84,24 +82,24 @@ dependencies { 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.6.2" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2" + 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-ktx:2.8.1" + implementation "androidx.work:work-runtime-ktx:2.9.0" implementation "com.github.satyan:sugar:1.5" - implementation "com.google.code.gson:gson:2.9.1" - implementation "com.squareup.okhttp3:okhttp:4.10.0" + implementation "com.google.code.gson:gson:2.10" + implementation "com.squareup.okhttp3:okhttp:4.11.0" implementation "com.dropbox.core:dropbox-core-sdk:5.4.4" // adding web dav support: https://github.com/thegrizzlylabs/sardine-android' implementation "com.github.guardianproject:sardine-android:89f7eae512" - implementation "com.google.android.material:material:1.10.0" + implementation "com.google.android.material:material:1.11.0" - implementation "com.github.bumptech.glide:glide:$versions.glide" - annotationProcessor "com.github.bumptech.glide:compiler:$versions.glide" + 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.github.stfalcon:frescoimageviewer:0.5.0" @@ -131,7 +129,7 @@ dependencies { exclude group: "com.squareup.okio", module: "okio" } - implementation "com.google.guava:guava:31.0.1-android" + 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' @@ -144,7 +142,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:5.0.1' // google drive api - implementation 'com.google.android.gms:play-services-auth:20.7.0' + implementation 'com.google.android.gms:play-services-auth:21.0.0' 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') 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 8aaa3f7e..51560572 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 @@ -81,6 +81,7 @@ class MainMediaFragment : Fragment() { BroadcastManager.unregister(requireContext(), mMessageReceiver) } + @Deprecated("Deprecated in Java") override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_delete -> { 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 a691a311..16336697 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 @@ -212,6 +212,7 @@ class SpaceSetupActivity : BaseActivity() { } } + @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) 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 44a22239..9b27ae55 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 @@ -84,6 +84,7 @@ class GDriveFragment : Fragment() { } } + @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) diff --git a/build.gradle b/build.gradle index 08724626..079210f0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,5 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - apply from: './dependencies.gradle' - apply from: './config.gradle' repositories { google() @@ -54,7 +52,7 @@ buildscript { classpath "com.neenbedankt.gradle.plugins:android-apt:1.8" classpath "com.testdroid:gradle:2.63.1" classpath "gradle.plugin.com.browserstack.gradle:browserstack-gradle-plugin:2.3.1" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21" } } diff --git a/config.gradle b/config.gradle deleted file mode 100644 index bdfe3dd9..00000000 --- a/config.gradle +++ /dev/null @@ -1,9 +0,0 @@ -ext.config = [ - buildToolsVersion: '33.0.2', - compileSdkVersion: 34, - minSdkVersion : 21, - targetSdkVersion : 34, - appcompat : 'com.android.support:appcompat-v7:28.0.0', - versionName : '0.3.1-alpha2', - versionCode : 20549 -] diff --git a/dependencies.gradle b/dependencies.gradle deleted file mode 100644 index 786591fb..00000000 --- a/dependencies.gradle +++ /dev/null @@ -1,6 +0,0 @@ -ext { - versions = [ - glide : '4.16.0', - kotlin : '1.8.22', - ] -} \ No newline at end of file From 8878875bf05e2300d15a652cc12961b9ca6bdce5 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Fri, 23 Feb 2024 15:08:46 -0800 Subject: [PATCH 13/63] chore(project): update issue templates and fastlane Bugs: include preconditions, actual behaviour, and more environment details Features: add alternate template for user story format Fastlane: use local ENV vars, remove slack and prepare for alpha/beta/internal uploads --- .github/ISSUE_TEMPLATE/bug_report.md | 30 ++++-- .github/ISSUE_TEMPLATE/feature_request.md | 60 ++++++++++- fastlane/Appfile | 2 +- fastlane/Fastfile | 123 ++++++++-------------- 4 files changed, 122 insertions(+), 93 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 849d1dd4..5e46cdcd 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,17 +1,20 @@ --- name: Bug report about: Create a report to document a bug in the app and to help us improve -title: '' +title: "" labels: bug -assignees: '' - +assignees: "" --- **Describe the bug** -A clear and concise description of what the bug is. +A clear and concise description of what the bug is, who it affects, where it happens, and when it occurs. + +**Preconditions** +Include any common configuration, users, or assumptions on how to reproduce. **To Reproduce** -Steps to reproduce the behavior: +Steps to reproduce the behavior. + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -20,13 +23,22 @@ Steps to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. +**Actual behaviour** +Describe the error, flaw, or failure when interacting in more detail. + **Screenshots** If applicable, add screenshots to help explain your problem. +**Logs** +If applicable, include any device logs. + **Environment (please complete the following information):** - - OS version: [e.g. Android 10] - - Device: [e.g. Samsung Galaxy A2] - - App Version [e.g. 2.2] + +- OS version: [e.g. Android 10] +- Device: [e.g. Samsung Galaxy A2] +- App Version [e.g. 2.2] +- Backend(s): [e.g. Internet Archive, Nextcloud] +- Component(s): [e.g. Onboarding, Settings] **Additional context** -Add any other context about the problem here. +Add any other context about the problem here. Please indicate priority and severity. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 5ecce628..10f303df 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +1,70 @@ --- name: Feature request -about: Suggest an idea for this project. Please provide your feedback using the 'I +about: + Suggest an idea for this project. Please provide your feedback using the 'I Like, I Wish, What if' feedback format -title: '' +title: "" labels: enhancement -assignees: '' - +assignees: "" --- -A preview of the [**I Like, I Wish, What if feedback format**](https://public-media.interaction-design.org/pdf/I-Like-I-Wish-What-If.pdf) +A preview of the [**I Like, I Wish, What if feedback format**](https://public-media.interaction-design.org/pdf/I-Like-I-Wish-What-If.pdf) to fascilitate discussions. **I Like...** + **I Wish...** + **What If...** + + +--- + +A preview of the **User Story format** + +## Motivation and Impact + +A clear reasoning of the value of the feature and why it should exist. + +**We believe** +**Will result** +**We can proceed when** + +## User Story (High-Level Acceptance Criteria) + +A description of high-level outcomes for this feature as an end-user. + +As a _(type of user)_, I want _(some goal)_ so that _(some reason)_. + +Given _(precondition)_, when _(action)_, then _(outcome)_. + +## User Flow (optional) + +A step-by-step run-through of the user's journey with screenshots or mockups. + +## Functional Requirements and Developer Notes + +A technical discussion about the requirements with diagrams, schemas, or references. + +## Quality Assurance and Security Notes + +Describe what could go wrong with the system and how it would be handled. + +## Not in Scope, Questions, and Answers + +Mention aspects of the feature that are not in scope. +Ask any remaining questions and answer known issues. + +## Acceptance Criteria + +A detailed list of rules that will consider this feature "done". + +## Affected + +Take note of any people, departments, users, platforms, or systems affected. + +Be sure to tag individuals or representatives. diff --git a/fastlane/Appfile b/fastlane/Appfile index 6c3cd0cf..d13a2606 100644 --- a/fastlane/Appfile +++ b/fastlane/Appfile @@ -1,2 +1,2 @@ -json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one +json_key_file(ENV["FASTLANE_JSON_KEY_FILE"]) # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one package_name("net.opendasharchive.openarchive") # e.g. com.krausefx.app diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7ceb693c..ab92687c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -5,30 +5,32 @@ platform :android do lane :test do gradle(task: "test") end - before_all do - ENV["SLACK_URL"] - end - desc "Submit a new Beta Build to Beta" - lane :beta do + desc "Submit a new Internal Build" + lane :internal do send_progress_message("Build Started :rocket:") gradle(task: "clean assembleRelease") - gradle(task: "assemble", build_type: "debug") - # sh "your_script.sh" - # You can also use other beta testing services here - send_progress_message("Uploading To Slack :rocket:") - upload_to_slack() + send_progress_message("Uploading To Internal track :rocket:") + upload_to_play_store(track: "internal") end - desc "Upload the latest output APK to #distribution Slack channel" - private_lane :upload_to_slack do |options| - file_path = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH] - file_name = file_path.gsub(/\/.*\//,"") - access_token = ENV["ACCESS_TOKEN"] - channel_name = "#distribution" - sh "echo Uploading " + file_name + " to Slack" - sh "curl https://slack.com/api/files.upload -F token=\"" + access_token + "\" -F channels=\"" + channel_name + "\" -F title=\"" + file_name + "\" -F filename=\"" + file_name + "\" -F file=@" + file_path + desc "Submit a new Alpha Build" + lane :alpha do + send_progress_message("Build Started :rocket:") + gradle(task: "clean assembleRelease") + + send_progress_message("Uploading To Alpha track :rocket:") + upload_to_play_store(track: "alpha") + end + + desc "Submit a new Beta Build" + lane :beta do + send_progress_message("Build Started :rocket:") + gradle(task: "clean assembleRelease") + + send_progress_message("Uploading To Beta track :rocket:") + upload_to_play_store(track: "beta") end desc "Deploy a new version to the Google Play" @@ -38,66 +40,31 @@ platform :android do end end - def send_message(message) - slack( - message: message, - success: true, - slack_url: ENV["SLACK_URL"], - attachment_properties: { - fields: [ - { - title: "Build number", - value: ENV["BUILD_NUMBER"], - } - ] - } - ) - end +def send_message(message) + puts message +end - def send_progress_message(message) - slack( - message: message, - success: true, - slack_url: ENV["SLACK_URL"], - ) - end +def send_progress_message(message) + puts message +end - def on_error(exception) - slack( - message: "Some thing goes wrong", - success: false, - slack_url: ENV["SLACK_URL"], - attachment_properties: { - fields: [ - { - title: "Build number", - value: ENV["BUILD_NUMBER"], - }, - { - title: "Error message", - value: exception.to_s, - short: false - } - ] - } - ) +def on_error(exception) + send_message "${exception}" +end - after_all do |lane| - file_name = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH].gsub(/\/.*\//,"") - slack( - message: "Successfully deployed new App Update! :champagne:", - channel: "@user.name,#channel.name", - default_payloads: [ - :git_branch, - :last_git_commit_hash, - :last_git_commit_message - ], - payload: { - # Optional, lets you specify any number of your own Slack attachments. - "Build Date" => Time.new.to_s, - "APK" => file_name - }, - success: true - ) - end -end \ No newline at end of file +after_all do |lane| + file_name = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH].gsub(/\/.*\//,"") + send_message "Successfully deployed new App Update! :champagne:" + default_payloads = [ + :git_branch, + :last_git_commit_hash, + :last_git_commit_message + ] + payload = { + "Build Date" => Time.new.to_s, + "APK" => file_name + } + send_message file_name + send_message "#{default_payloads}" + send_message "#{payload}" +end From b6a0799e621060ca6260e61a08470d6dab0ae698 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 20 Feb 2024 10:35:57 -0800 Subject: [PATCH 14/63] fix(ci): just make circle ci pass for now --- .circleci/config.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d3b31ac9..d9867073 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,16 +3,16 @@ jobs: build: working_directory: ~/code docker: - - image: circleci/android:api-30 + - image: cimg/android:2024.01 environment: JVM_OPTS: -Xmx3200m steps: - checkout - restore_cache: key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} -# - run: -# name: Chmod permissions #if permission for Gradlew Dependencies fail, use this. -# command: sudo chmod +x ./gradlew + # - run: + # name: Chmod permissions #if permission for Gradlew Dependencies fail, use this. + # command: sudo chmod +x ./gradlew - run: name: Create local.properties command: touch local.properties @@ -26,10 +26,9 @@ jobs: - run: name: Run Tests command: ./gradlew lint test - - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ path: app/build/reports destination: reports - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ path: app/build/test-results # See https://circleci.com/docs/2.0/deployment-integrations/ for deploy examples - From 02a2d6eda08311027c7616eb0c6245d489fadc38 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Mon, 26 Feb 2024 15:49:00 -0800 Subject: [PATCH 15/63] Revert "fix(tor): ensure registered recievers are not exported in android 14+" This reverts commit 32124f32071fd65d54cad77438e81dabce8928e0. --- .../openarchive/upload/BroadcastManager.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt index 3cd2d11e..7d3ec687 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt @@ -2,11 +2,8 @@ package net.opendasharchive.openarchive.upload import android.content.BroadcastReceiver import android.content.Context -import android.content.Context.RECEIVER_NOT_EXPORTED import android.content.Intent import android.content.IntentFilter -import android.os.Build -import androidx.core.content.ContextCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager object BroadcastManager { @@ -40,10 +37,11 @@ object BroadcastManager { } fun register(context: Context, receiver: BroadcastReceiver) { - LocalBroadcastManager.getInstance(context).apply { - ContextCompat.registerReceiver(context, receiver, IntentFilter(Action.Change.id), ContextCompat.RECEIVER_NOT_EXPORTED) - ContextCompat.registerReceiver(context, receiver, IntentFilter(Action.Delete.id), ContextCompat.RECEIVER_NOT_EXPORTED) - } + LocalBroadcastManager.getInstance(context) + .registerReceiver(receiver, IntentFilter(Action.Change.id)) + + LocalBroadcastManager.getInstance(context) + .registerReceiver(receiver, IntentFilter(Action.Delete.id)) } fun unregister(context: Context, receiver: BroadcastReceiver) { From bdb515f647d45062836cd461c7da77263181e4b8 Mon Sep 17 00:00:00 2001 From: RJ Date: Mon, 26 Feb 2024 16:23:32 -0800 Subject: [PATCH 16/63] fix(uploads): change progress text color (#557) --- app/src/main/res/layout/rv_media_box.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/rv_media_box.xml b/app/src/main/res/layout/rv_media_box.xml index 8d0e4826..01db3ed8 100644 --- a/app/src/main/res/layout/rv_media_box.xml +++ b/app/src/main/res/layout/rv_media_box.xml @@ -87,6 +87,7 @@ android:text="100%" android:textSize="14sp" android:visibility="gone" + android:textColor="@color/colorPrimary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" From caf1a3452d247c143e0eccd922076be09a74f359 Mon Sep 17 00:00:00 2001 From: RJ Date: Mon, 26 Feb 2024 16:37:21 -0800 Subject: [PATCH 17/63] fix(webdav): fix folder separator in webdav not being created (#558) The root problem is the '/' separator is being url encoded and not sa url path. --- app/build.gradle | 1 + .../openarchive/features/main/MainActivity.kt | 13 ++++++------ .../services/webdav/WebDavConduit.kt | 20 +++++++++++++++++-- gradle.properties | 1 - 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2535cc9a..6b9b0741 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,6 +63,7 @@ android { buildFeatures { viewBinding true + buildConfig true } lint { abortOnError false 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 34e4aefc..b822f725 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 @@ -13,14 +13,13 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.TooltipCompat import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.ViewPager2 import com.esafirm.imagepicker.features.ImagePickerLauncher import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.SnackbarLayout -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import net.opendasharchive.openarchive.FolderAdapter import net.opendasharchive.openarchive.FolderAdapterListener @@ -128,8 +127,10 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener mLastMediaItem = position } - updateBottomNavbar(position) - refreshCurrentProject() + lifecycleScope.launch(Dispatchers.Main) { + updateBottomNavbar(position) + refreshCurrentProject() + } } override fun onPageScrollStateChanged(state: Int) {} @@ -376,10 +377,10 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener mSnackBar?.show() - CoroutineScope(Dispatchers.IO).launch { + lifecycleScope.launch(Dispatchers.IO) { val media = Picker.import(this@MainActivity, getSelectedProject(), uri) - MainScope().launch { + lifecycleScope.launch(Dispatchers.Main) { mSnackBar?.dismiss() intent = null diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt index df0a0567..3312eec2 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt @@ -18,7 +18,7 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { override suspend fun upload(): Boolean { val space = mMedia.space ?: return false val base = space.hostUrl ?: return false - val path = getPath() ?: return false + val path = getPath()?.sanitizeUrlPaths() ?: return false mClient = SaveClient.getSardine(mContext, space) @@ -27,7 +27,10 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { val fileName = getUploadFileName(mMedia) try { - createFolders(base, path) + + // webdav can support one-shot folder path creation + val url = construct(base, path) + createFolder(url) uploadMetadata(base, path, fileName) } @@ -81,6 +84,7 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { if (!mClient.exists(url)) mClient.createDirectory(url) } + @Throws(IOException::class) private suspend fun uploadChunked(base: HttpUrl, path: List, fileName: String): Boolean { val space = mMedia.space ?: return false @@ -189,4 +193,16 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { false, null) } } + + // split paths specified by the user. + private fun List.sanitizeUrlPaths(): List { + return this.fold(mutableListOf()) { res, it -> + if (it.contains('/')) { + res.addAll(it.split('/')) + } else { + res.add(it) + } + res + } + } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index af09e692..f9916a37 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # org.gradle.parallel=true android.useAndroidX=true android.enableJetifier=true -android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=true android.nonFinalResIds=true From ef23aa83b230480ee516439188547ffee31d7662 Mon Sep 17 00:00:00 2001 From: RJ Date: Mon, 26 Feb 2024 17:07:14 -0800 Subject: [PATCH 18/63] fix(media): fix accessibility colors on upload count (#559) --- app/src/main/res/layout/activity_main.xml | 2 ++ app/src/main/res/values-night/colors.xml | 1 + app/src/main/res/values/colors.xml | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5fce1099..b5d935f2 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -70,6 +70,7 @@ android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginBottom="4dp" + tools:text="folder name" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/current_folder_icon" app:layout_constraintTop_toTopOf="parent" /> @@ -80,6 +81,7 @@ android:layout_height="match_parent" android:layout_marginStart="8dp" android:background="@drawable/pill" + tools:text="1" android:gravity="center" android:paddingStart="8dp" android:paddingEnd="8dp" diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 3d6158d3..e15a853a 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -19,4 +19,5 @@ @color/c23_medium_grey @color/c23_darker_grey + #434343 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index cb1f6d40..ff1e5267 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -91,7 +91,7 @@ --> - @color/extra_light_grey + #E3E3E4 @color/white @color/red @color/light_grey From fc7e162733df8f9f69c717cde91a36ba0b5b5a1a Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Fri, 1 Mar 2024 17:24:52 -0800 Subject: [PATCH 19/63] Revert "fix(tor): ensure registered recievers are not exported in android 14+" This reverts commit 32124f32071fd65d54cad77438e81dabce8928e0. --- .../openarchive/upload/BroadcastManager.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt index 3cd2d11e..7d3ec687 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt @@ -2,11 +2,8 @@ package net.opendasharchive.openarchive.upload import android.content.BroadcastReceiver import android.content.Context -import android.content.Context.RECEIVER_NOT_EXPORTED import android.content.Intent import android.content.IntentFilter -import android.os.Build -import androidx.core.content.ContextCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager object BroadcastManager { @@ -40,10 +37,11 @@ object BroadcastManager { } fun register(context: Context, receiver: BroadcastReceiver) { - LocalBroadcastManager.getInstance(context).apply { - ContextCompat.registerReceiver(context, receiver, IntentFilter(Action.Change.id), ContextCompat.RECEIVER_NOT_EXPORTED) - ContextCompat.registerReceiver(context, receiver, IntentFilter(Action.Delete.id), ContextCompat.RECEIVER_NOT_EXPORTED) - } + LocalBroadcastManager.getInstance(context) + .registerReceiver(receiver, IntentFilter(Action.Change.id)) + + LocalBroadcastManager.getInstance(context) + .registerReceiver(receiver, IntentFilter(Action.Delete.id)) } fun unregister(context: Context, receiver: BroadcastReceiver) { From 9a8f43e36c444e9ba502a750cfc22d0579a257ec Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Fri, 1 Mar 2024 17:29:16 -0800 Subject: [PATCH 20/63] Revert "fix(webdav): fix folder separator in webdav not being created (#558)" This reverts commit caf1a3452d247c143e0eccd922076be09a74f359. --- app/build.gradle | 1 - .../openarchive/features/main/MainActivity.kt | 13 ++++++------ .../services/webdav/WebDavConduit.kt | 20 ++----------------- gradle.properties | 1 + 4 files changed, 9 insertions(+), 26 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6b9b0741..2535cc9a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,7 +63,6 @@ android { buildFeatures { viewBinding true - buildConfig true } lint { abortOnError false 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 b822f725..34e4aefc 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 @@ -13,13 +13,14 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.TooltipCompat import androidx.core.content.ContextCompat -import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.ViewPager2 import com.esafirm.imagepicker.features.ImagePickerLauncher import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.SnackbarLayout +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import net.opendasharchive.openarchive.FolderAdapter import net.opendasharchive.openarchive.FolderAdapterListener @@ -127,10 +128,8 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener mLastMediaItem = position } - lifecycleScope.launch(Dispatchers.Main) { - updateBottomNavbar(position) - refreshCurrentProject() - } + updateBottomNavbar(position) + refreshCurrentProject() } override fun onPageScrollStateChanged(state: Int) {} @@ -377,10 +376,10 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener mSnackBar?.show() - lifecycleScope.launch(Dispatchers.IO) { + CoroutineScope(Dispatchers.IO).launch { val media = Picker.import(this@MainActivity, getSelectedProject(), uri) - lifecycleScope.launch(Dispatchers.Main) { + MainScope().launch { mSnackBar?.dismiss() intent = null diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt index 3312eec2..df0a0567 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt @@ -18,7 +18,7 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { override suspend fun upload(): Boolean { val space = mMedia.space ?: return false val base = space.hostUrl ?: return false - val path = getPath()?.sanitizeUrlPaths() ?: return false + val path = getPath() ?: return false mClient = SaveClient.getSardine(mContext, space) @@ -27,10 +27,7 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { val fileName = getUploadFileName(mMedia) try { - - // webdav can support one-shot folder path creation - val url = construct(base, path) - createFolder(url) + createFolders(base, path) uploadMetadata(base, path, fileName) } @@ -84,7 +81,6 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { if (!mClient.exists(url)) mClient.createDirectory(url) } - @Throws(IOException::class) private suspend fun uploadChunked(base: HttpUrl, path: List, fileName: String): Boolean { val space = mMedia.space ?: return false @@ -193,16 +189,4 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { false, null) } } - - // split paths specified by the user. - private fun List.sanitizeUrlPaths(): List { - return this.fold(mutableListOf()) { res, it -> - if (it.contains('/')) { - res.addAll(it.split('/')) - } else { - res.add(it) - } - res - } - } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index f9916a37..af09e692 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,6 +18,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # org.gradle.parallel=true android.useAndroidX=true android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=true android.nonFinalResIds=true From 11ae32a11b30317ce19ba1f6a47936836467738d Mon Sep 17 00:00:00 2001 From: RJ Date: Mon, 4 Mar 2024 11:04:03 -0800 Subject: [PATCH 21/63] fix(onboarding): arrow graphic is hidden on small width devices (#567) the device reported is 74mm wide which is around 466dp. use a vertical orientation by default and anything greater than 466dp use a horizontal orientation. tested on small and medium emulators. Such a good use case for jetpack compose. --- .../layout-sw480dp/activity_onboarding23.xml | 74 +++++++++++++++++++ .../main/res/layout/activity_onboarding23.xml | 7 +- 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/layout-sw480dp/activity_onboarding23.xml diff --git a/app/src/main/res/layout-sw480dp/activity_onboarding23.xml b/app/src/main/res/layout-sw480dp/activity_onboarding23.xml new file mode 100644 index 00000000..fe88a434 --- /dev/null +++ b/app/src/main/res/layout-sw480dp/activity_onboarding23.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_onboarding23.xml b/app/src/main/res/layout/activity_onboarding23.xml index fe88a434..f1792b06 100644 --- a/app/src/main/res/layout/activity_onboarding23.xml +++ b/app/src/main/res/layout/activity_onboarding23.xml @@ -43,9 +43,10 @@ android:id="@+id/get_started" android:layout_width="match_parent" android:layout_height="wrap_content" + android:gravity="center" android:clickable="true" android:focusable="true" - android:orientation="horizontal" + android:orientation="vertical" android:paddingLeft="20dp" android:paddingRight="20dp" android:paddingBottom="10dp"> @@ -61,9 +62,9 @@ From ca4188c2d3924dbb6181b3912a210090695104de Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Mon, 4 Mar 2024 12:32:34 -0800 Subject: [PATCH 22/63] fix(build): add compose and dependencies --- app/build.gradle | 25 ++++++++++++++++++++++--- build.gradle | 2 +- gradle.properties | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2535cc9a..a180a0ac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,18 +63,25 @@ android { buildFeatures { viewBinding true + buildConfig true + compose true } + lint { abortOnError false } + composeOptions { + kotlinCompilerExtensionVersion "1.5.10" + } + namespace 'net.opendasharchive.openarchive' } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.21" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1" + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0" implementation "androidx.core:core-ktx:1.12.0" implementation "androidx.appcompat:appcompat:1.6.1" @@ -87,6 +94,18 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.1' implementation "androidx.work:work-runtime-ktx:2.9.0" + implementation "androidx.compose.ui:ui:1.6.2" + implementation "androidx.compose.material:material:1.6.2" + implementation 'androidx.compose.foundation:foundation:1.6.2' + implementation "androidx.compose.ui:ui-tooling-preview:1.6.2" + implementation "androidx.activity:activity-compose:1.8.2" + 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" @@ -113,7 +132,7 @@ dependencies { implementation "info.guardianproject.netcipher:netcipher:2.2.0-alpha" //from here: https://github.com/guardianproject/proofmode - implementation ("org.proofmode:android-libproofmode:1.0.26") { + implementation("org.proofmode:android-libproofmode:1.0.26") { transitive = false diff --git a/build.gradle b/build.gradle index 079210f0..72c6e368 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ buildscript { classpath "com.neenbedankt.gradle.plugins:android-apt:1.8" classpath "com.testdroid:gradle:2.63.1" classpath "gradle.plugin.com.browserstack.gradle:browserstack-gradle-plugin:2.3.1" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22" } } diff --git a/gradle.properties b/gradle.properties index af09e692..82e420b5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # org.gradle.parallel=true android.useAndroidX=true android.enableJetifier=true -android.defaults.buildfeatures.buildconfig=true +#android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=true android.nonFinalResIds=true From 48545687279119f060f7250cb13d8d42a8c6c11c Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Mon, 4 Mar 2024 18:07:36 -0800 Subject: [PATCH 23/63] fix(internetarchive): use xauthn service to simplify InternetArchive the auth keys are returned by a webservice using email/password a simple link is provided to create an account --- app/src/main/AndroidManifest.xml | 3 + .../opendasharchive/openarchive/SaveApp.kt | 11 +- .../openarchive/core/di/CoreModule.kt | 7 + .../openarchive/core/di/FeaturesModule.kt | 8 + .../openarchive/core/state/StateDispatcher.kt | 30 ++++ .../openarchive/core/state/StateListener.kt | 13 ++ .../features/internetarchive/Module.kt | 15 ++ .../domain/model/InternetArchiveAuth.kt | 6 + .../usecase/InternetArchiveLoginUseCase.kt | 4 + .../datasource/InternetArchiveRemoteSource.kt | 56 +++++++ .../mapping/InternetArchiveMapper.kt | 11 ++ .../model/InternetArchiveLoginRequest.kt | 6 + .../model/InternetArchiveLoginResponse.kt | 21 +++ .../repository/InternetArchiveRepository.kt | 20 +++ .../presentation/InternetArchiveActivity.kt | 16 ++ .../login/InternetArchiveLoginScreen.kt | 151 ++++++++++++++++++ .../login/InternetArchiveLoginState.kt | 12 ++ .../login/InternetArchiveLoginViewModel.kt | 71 ++++++++ .../openarchive/features/main/MainActivity.kt | 5 +- .../openarchive/services/SaveClient.kt | 2 +- 20 files changed, 465 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/di/FeaturesModule.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/state/StateListener.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginRequest.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ac3552ee..56ab137e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -120,6 +120,9 @@ android:label="@string/title_activity_login" android:taskAffinity="" /> + diff --git a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt index 91196213..c44df32f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt @@ -6,8 +6,12 @@ import com.facebook.imagepipeline.core.ImagePipelineConfig import com.facebook.imagepipeline.decoder.SimpleProgressiveJpegConfig import com.orm.SugarApp import info.guardianproject.netcipher.proxy.OrbotHelper +import net.opendasharchive.openarchive.core.di.coreModule +import net.opendasharchive.openarchive.core.di.featuresModule import net.opendasharchive.openarchive.util.Prefs import net.opendasharchive.openarchive.util.Theme +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin import timber.log.Timber class SaveApp : SugarApp() { @@ -19,6 +23,11 @@ class SaveApp : SugarApp() { override fun onCreate() { super.onCreate() + startKoin { + androidContext(this@SaveApp) + modules(coreModule, featuresModule) + } + val config = ImagePipelineConfig.newBuilder(this) .setProgressiveJpegConfig(SimpleProgressiveJpegConfig()) .setResizeAndRotateEnabledForNetwork(true) @@ -50,4 +59,4 @@ class SaveApp : SugarApp() { oh.init() } -} \ No newline at end of file +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt b/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt new file mode 100644 index 00000000..a9d91456 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt @@ -0,0 +1,7 @@ +package net.opendasharchive.openarchive.core.di + +import org.koin.dsl.module + +val coreModule = module { + +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/di/FeaturesModule.kt b/app/src/main/java/net/opendasharchive/openarchive/core/di/FeaturesModule.kt new file mode 100644 index 00000000..54cb1852 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/di/FeaturesModule.kt @@ -0,0 +1,8 @@ +package net.opendasharchive.openarchive.core.di + +import net.opendasharchive.openarchive.features.internetarchive.internetArchiveModule +import org.koin.dsl.module + +val featuresModule = module { + includes(internetArchiveModule) +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt new file mode 100644 index 00000000..a995fea5 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt @@ -0,0 +1,30 @@ +package net.opendasharchive.openarchive.core.state + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.launch + +typealias Reducer = (T, A) -> T +typealias Effect = suspend (T, A) -> Unit + +typealias Dispatch = (A) -> Unit + +class StateDispatcher( + initialState: T, + private val reducer: Reducer, + private val effects: Effect +) { + private val scope = CoroutineScope(SupervisorJob()) + + private val _state = MutableStateFlow(initialState) + val state = _state + + fun dispatch(action: A) { + val state = _state.updateAndGet { reducer(it, action) } + scope.launch { + effects(state, action) + } + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateListener.kt b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateListener.kt new file mode 100644 index 00000000..97c89cf5 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateListener.kt @@ -0,0 +1,13 @@ +package net.opendasharchive.openarchive.core.state + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow + +class StateListener { + private val _actions = Channel() + val actions = _actions.receiveAsFlow() + + suspend fun send(action: T) { + _actions.send(action) + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt new file mode 100644 index 00000000..d6853da7 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -0,0 +1,15 @@ +package net.opendasharchive.openarchive.features.internetarchive + +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val internetArchiveModule = module { + factory { InternetArchiveRemoteSource(get()) } + factory { InternetArchiveMapper() } + single { InternetArchiveRepository(get(), get()) } + viewModel { InternetArchiveLoginViewModel(get()) } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt new file mode 100644 index 00000000..c49806df --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt @@ -0,0 +1,6 @@ +package net.opendasharchive.openarchive.features.internetarchive.domain.model + +data class InternetArchiveAuth( + val access: String, + val secret: String, +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt new file mode 100644 index 00000000..0315feea --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt @@ -0,0 +1,4 @@ +package net.opendasharchive.openarchive.features.internetarchive.domain.usecase + +class InternetArchiveLoginUseCase { +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt new file mode 100644 index 00000000..73f355a9 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt @@ -0,0 +1,56 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource + +import android.content.Context +import com.google.gson.Gson +import kotlinx.coroutines.suspendCancellableCoroutine +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse +import net.opendasharchive.openarchive.services.SaveClient +import okhttp3.Call +import okhttp3.Callback +import okhttp3.FormBody +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +private val LOGIN_URI = "https://archive.org/services/xauthn?op=login" + +class InternetArchiveRemoteSource( + private val context: Context +) { + + suspend fun login(request: InternetArchiveLoginRequest): Result { + val client = SaveClient.get(context) + return suspendCancellableCoroutine { continuation -> + client.newCall( + Request.Builder() + .url(LOGIN_URI) + .post( + FormBody.Builder().add("email", request.email) + .add("password", request.password).build() + ) + .build() + ).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + continuation.resumeWithException(e) + } + + override fun onResponse(call: Call, response: Response) { + val data = + Gson().fromJson( + response.body?.string(), + InternetArchiveLoginResponse::class.java + ) + continuation.resume(Result.success(data)) + } + + }) + + continuation.invokeOnCancellation { + client.dispatcher.cancelAll() + } + } + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt new file mode 100644 index 00000000..314ca2c9 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt @@ -0,0 +1,11 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping + +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse + +class InternetArchiveMapper { + + fun loginToAuth(response: InternetArchiveLoginResponse) = InternetArchiveAuth( + access = response.values.s3!!.access, secret = response.values.s3.secret + ) +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginRequest.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginRequest.kt new file mode 100644 index 00000000..9e9f9889 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginRequest.kt @@ -0,0 +1,6 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.model + +data class InternetArchiveLoginRequest( + val email: String, + val password: String, +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt new file mode 100644 index 00000000..b81167eb --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt @@ -0,0 +1,21 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.model + +data class InternetArchiveLoginResponse( + val success: Boolean, + val values: Values, + val version: Int, +) { + data class Values( + val expires: String? = null, + val s3: S3? = null, + val screenname: String? = null, + val email: String? = null, + val itemname: String? = null, + val reason: String? = null + ) + + data class S3( + val access: String, + val secret: String, + ) +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt new file mode 100644 index 00000000..7a595a46 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt @@ -0,0 +1,20 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository + +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest + +class InternetArchiveRepository( + private val remoteSource: InternetArchiveRemoteSource, + private val mapper: InternetArchiveMapper +) { + suspend fun login(email: String, password: String): Result = remoteSource.login( + InternetArchiveLoginRequest(email, password) + ).mapCatching { + if (it.success.not()) { + throw IllegalArgumentException(it.values.reason) + } + mapper.loginToAuth(it) + } +} 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 new file mode 100644 index 00000000..23ab0820 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt @@ -0,0 +1,16 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen + +class InternetArchiveActivity: AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + InternetArchiveLoginScreen() + } + } +} 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 new file mode 100644 index 00000000..e4084bd2 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -0,0 +1,151 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.login + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.state.Dispatch +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword +import org.koin.androidx.compose.koinViewModel + +@Composable +fun InternetArchiveLoginScreen() { + val viewModel: InternetArchiveLoginViewModel = koinViewModel() + + val state by viewModel.state.collectAsState() + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = {} + ) + + LaunchedEffect(Unit) { + viewModel.effects.collect { action -> + when (action) { + is CreateLogin -> launcher.launch( + Intent( + Intent.ACTION_VIEW, + Uri.parse(CreateLogin.URI) + ) + ) + + else -> Unit + } + } + } + + InternetArchiveLoginContent(state, viewModel::dispatch) +} + +@Composable +private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispatch: Dispatch) { + + Box(modifier = Modifier.fillMaxSize()) { + Column( + Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + state.auth?.let { auth -> + Text(text = "${auth.access}:${auth.secret}", + modifier = Modifier.padding(bottom = 20.dp), + color = Color.Red) + } + + Text( + text = stringResource(id = R.string.internet_archive), + fontSize = 32.sp, + modifier = Modifier.padding(bottom = 20.dp) + ) + + TextField( + value = state.email, + onValueChange = { dispatch(UpdateEmail(it)) }, + label = { Text(stringResource(id = R.string.prompt_email)) }, + placeholder = { Text(stringResource(id = R.string.prompt_email)) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + autoCorrect = false, + keyboardType = KeyboardType.Email + ), + isError = state.isEmailError + ) + + TextField( + value = state.password, onValueChange = { dispatch(UpdatePassword(it)) }, + label = { Text(stringResource(id = R.string.prompt_password)) }, + placeholder = { Text(stringResource(id = R.string.prompt_password)) }, + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + autoCorrect = false, + imeAction = ImeAction.Go + ), + isError = state.isPasswordError + ) + + if (state.isLoginError) { + Text( + modifier = Modifier.padding(top = 20.dp), + text = stringResource(id = R.string.error_incorrect_username_or_password), + color = MaterialTheme.colors.error + ) + } + + Button( + modifier = Modifier.padding(top = 20.dp), + onClick = { dispatch(Login) }) { + Text(stringResource(id = R.string.title_activity_login)) + } + + TextButton(onClick = { dispatch(CreateLogin) }) { + Text("Create Login") + } + } + } +} + +@Composable +@Preview +private fun InternetArchiveLoginPreview() { + InternetArchiveLoginContent( + state = InternetArchiveLoginState( + email = "user@example.org", + password = "123abc" + ) + ) {} +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt new file mode 100644 index 00000000..767e2199 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt @@ -0,0 +1,12 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.login + +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth + +data class InternetArchiveLoginState( + val email: String = "", + val password: String = "", + val isEmailError: Boolean = false, + val isPasswordError: Boolean = false, + val isLoginError: Boolean = false, + val auth: InternetArchiveAuth? = null +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt new file mode 100644 index 00000000..41006cd1 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -0,0 +1,71 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.login + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.opendasharchive.openarchive.core.state.StateDispatcher +import net.opendasharchive.openarchive.core.state.StateListener +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginError +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword + +class InternetArchiveLoginViewModel( + private val repository: InternetArchiveRepository +) : ViewModel() { + private val dispatcher = StateDispatcher(InternetArchiveLoginState(), ::reduce, ::effects) + private val listener = StateListener() + + val state = dispatcher.state + val effects = listener.actions + + private fun reduce(state: InternetArchiveLoginState, action: Action): InternetArchiveLoginState = when(action) { + is UpdateEmail -> state.copy(email = action.value) + is UpdatePassword -> state.copy(password = action.value) + is LoginError -> state.copy(isLoginError = true) + is LoginSuccess -> state.copy(auth = action.value) + else -> state + } + + private suspend fun effects(state: InternetArchiveLoginState, action: Action) { + when(action) { + is Login -> withContext(Dispatchers.IO) { + repository.login(state.email, state.password) + .onSuccess { + dispatcher.dispatch(LoginSuccess(it)) + }.onFailure { + dispatcher.dispatch(LoginError(it)) + } + } + is CreateLogin -> listener.send(action) + else -> Unit + } + } + + fun dispatch(action: Action) { + viewModelScope.launch { + dispatcher.dispatch(action) + } + } + + sealed interface Action { + data object Login: Action + + data class LoginSuccess(val value: InternetArchiveAuth): Action + + data class LoginError(val value: Throwable): Action + + data object CreateLogin: Action { + const val URI = "https://archive.org/account/signup" + } + + data class UpdateEmail(val value: String): Action + data class UpdatePassword(val value: String): Action + } +} 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 34e4aefc..9e97272c 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 @@ -32,6 +32,7 @@ import net.opendasharchive.openarchive.db.Project import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.features.folders.AddFolderActivity +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveActivity import net.opendasharchive.openarchive.features.media.AddMediaDialogFragment import net.opendasharchive.openarchive.features.media.Picker import net.opendasharchive.openarchive.features.media.PreviewActivity @@ -458,7 +459,9 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener mBinding.spacesCard.hide() } - startActivity(Intent(this, SpaceSetupActivity::class.java)) + //startActivity(Intent(this, SpaceSetupActivity::class.java)) + + startActivity(Intent(this, InternetArchiveActivity::class.java)) } override fun getSelectedSpace(): Space? { diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt index 95802892..42912156 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt @@ -158,4 +158,4 @@ class SaveClient(context: Context) : StrongBuilderBase return sardine } } -} \ No newline at end of file +} From c364b751f26484680b1f8ad7377cceea7dfacd8e Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 5 Mar 2024 13:24:26 -0800 Subject: [PATCH 24/63] fix(ia): integrating xauthn and compose flow into space setup --- .../infrastructure/client/ClientResult.kt | 29 +++++++++ .../core/presentation/StatefulViewModel.kt | 25 +++++++ .../openarchive/core/state/StateDispatcher.kt | 7 +- .../features/internetarchive/Module.kt | 2 +- .../domain/model/InternetArchive.kt | 8 +++ .../datasource/InternetArchiveRemoteSource.kt | 65 +++++++++---------- .../mapping/InternetArchiveMapper.kt | 12 +++- .../repository/InternetArchiveRepository.kt | 23 ++++--- .../presentation/InternetArchiveActivity.kt | 13 +++- .../presentation/InternetArchiveFragment.kt | 54 +++++++++++++++ .../presentation/InternetArchiveScreen.kt | 60 +++++++++++++++++ .../presentation/InternetArchiveState.kt | 7 ++ .../presentation/InternetArchiveViewModel.kt | 23 +++++++ .../login/InternetArchiveLoginScreen.kt | 22 +++---- .../login/InternetArchiveLoginState.kt | 18 ++++- .../login/InternetArchiveLoginViewModel.kt | 58 ++++++++--------- .../openarchive/features/main/MainActivity.kt | 5 +- .../features/onboarding/SpaceSetupActivity.kt | 4 +- 18 files changed, 329 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/presentation/StatefulViewModel.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt b/app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt new file mode 100644 index 00000000..9c81c173 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt @@ -0,0 +1,29 @@ +package net.opendasharchive.openarchive.core.infrastructure.client + +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + suspend fun OkHttpClient.enqueueResult( + request: Request, + onResume: (Response) -> T +) = suspendCancellableCoroutine { continuation -> + newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + continuation.resumeWithException(e) + } + + override fun onResponse(call: Call, response: Response) { + continuation.resume(onResume(response)) + } + }) + + continuation.invokeOnCancellation { + dispatcher.cancelAll() + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/StatefulViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/StatefulViewModel.kt new file mode 100644 index 00000000..1cc6258b --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/StatefulViewModel.kt @@ -0,0 +1,25 @@ +package net.opendasharchive.openarchive.core.presentation + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import net.opendasharchive.openarchive.core.state.StateDispatcher +import net.opendasharchive.openarchive.core.state.StateListener + +abstract class StatefulViewModel( + initialState: State, +) : ViewModel() { + private val dispatcher = + StateDispatcher(viewModelScope, initialState, ::reduce, ::effects) + private val listener = StateListener() + + val state = dispatcher.state + val effects = listener.actions + + abstract fun reduce(state: State, action: Action): State + + abstract suspend fun effects(state: State, action: Action) + + fun dispatch(action: Action) = dispatcher.dispatch(action) + + suspend fun send(action: Action) = listener.send(action) +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt index a995fea5..a7e0758d 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt @@ -1,7 +1,7 @@ package net.opendasharchive.openarchive.core.state import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch @@ -12,18 +12,17 @@ typealias Effect = suspend (T, A) -> Unit typealias Dispatch = (A) -> Unit class StateDispatcher( + private val scope: CoroutineScope, initialState: T, private val reducer: Reducer, private val effects: Effect ) { - private val scope = CoroutineScope(SupervisorJob()) - private val _state = MutableStateFlow(initialState) val state = _state fun dispatch(action: A) { val state = _state.updateAndGet { reducer(it, action) } - scope.launch { + scope.launch(Dispatchers.Default) { effects(state, action) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index d6853da7..4b8d3451 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -11,5 +11,5 @@ val internetArchiveModule = module { factory { InternetArchiveRemoteSource(get()) } factory { InternetArchiveMapper() } single { InternetArchiveRepository(get(), get()) } - viewModel { InternetArchiveLoginViewModel(get()) } + viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt new file mode 100644 index 00000000..c6c2130e --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt @@ -0,0 +1,8 @@ +package net.opendasharchive.openarchive.features.internetarchive.domain.model + +data class InternetArchive( + val username: String, + val email: String, + val expires: String, + val auth: InternetArchiveAuth +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt index 73f355a9..5f48e8fd 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt @@ -2,18 +2,14 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure. import android.content.Context import com.google.gson.Gson -import kotlinx.coroutines.suspendCancellableCoroutine +import net.opendasharchive.openarchive.core.infrastructure.client.enqueueResult +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse import net.opendasharchive.openarchive.services.SaveClient -import okhttp3.Call -import okhttp3.Callback +import net.opendasharchive.openarchive.services.internetarchive.IaConduit.Companion.ARCHIVE_API_ENDPOINT import okhttp3.FormBody import okhttp3.Request -import okhttp3.Response -import java.io.IOException -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException private val LOGIN_URI = "https://archive.org/services/xauthn?op=login" @@ -21,36 +17,33 @@ class InternetArchiveRemoteSource( private val context: Context ) { - suspend fun login(request: InternetArchiveLoginRequest): Result { - val client = SaveClient.get(context) - return suspendCancellableCoroutine { continuation -> - client.newCall( - Request.Builder() - .url(LOGIN_URI) - .post( - FormBody.Builder().add("email", request.email) - .add("password", request.password).build() - ) - .build() - ).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - continuation.resumeWithException(e) - } + private val gson = Gson() - override fun onResponse(call: Call, response: Response) { - val data = - Gson().fromJson( - response.body?.string(), - InternetArchiveLoginResponse::class.java - ) - continuation.resume(Result.success(data)) - } - - }) + suspend fun login(request: InternetArchiveLoginRequest): Result = + SaveClient.get(context).enqueueResult( + Request.Builder() + .url(LOGIN_URI) + .post( + FormBody.Builder().add("email", request.email) + .add("password", request.password).build() + ) + .build() + ) { response -> + val data = gson.fromJson( + response.body?.string(), + InternetArchiveLoginResponse::class.java + ) + Result.success(data) + } - continuation.invokeOnCancellation { - client.dispatcher.cancelAll() - } + suspend fun testConnection(auth: InternetArchiveAuth): Result = + SaveClient.get(context).enqueueResult( + Request.Builder() + .url(ARCHIVE_API_ENDPOINT) + .method("GET", null) + .addHeader("Authorization", "LOW ${auth.access}:${auth.secret}") + .build() + ) { response -> + Result.success(response.isSuccessful) } - } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt index 314ca2c9..f3e4c407 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt @@ -1,11 +1,19 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse class InternetArchiveMapper { - fun loginToAuth(response: InternetArchiveLoginResponse) = InternetArchiveAuth( - access = response.values.s3!!.access, secret = response.values.s3.secret + private fun toAuth(response: InternetArchiveLoginResponse.S3) = InternetArchiveAuth( + access = response.access, secret = response.secret + ) + + fun toDomain(response: InternetArchiveLoginResponse.Values) = InternetArchive( + username = response.screenname ?: response.itemname ?: "", + email = response.email ?: "", + expires = response.expires ?: "", + auth = response.s3?.let { toAuth(it) } ?: InternetArchiveAuth("", "") ) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt index 7a595a46..5dc2d6d7 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt @@ -1,6 +1,8 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest @@ -9,12 +11,17 @@ class InternetArchiveRepository( private val remoteSource: InternetArchiveRemoteSource, private val mapper: InternetArchiveMapper ) { - suspend fun login(email: String, password: String): Result = remoteSource.login( - InternetArchiveLoginRequest(email, password) - ).mapCatching { - if (it.success.not()) { - throw IllegalArgumentException(it.values.reason) + suspend fun login(email: String, password: String): Result = + withContext(Dispatchers.IO) { + remoteSource.login( + InternetArchiveLoginRequest(email, password) + ).mapCatching { + if (it.success.not()) { + throw IllegalArgumentException(it.values.reason) + } + when(it.version) { + else -> mapper.toDomain(it.values) + } + } } - mapper.loginToAuth(it) - } } 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 23ab0820..a23b9471 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 @@ -3,14 +3,23 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace +import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveFragment.Companion.ARG_VAL_NEW_SPACE class InternetArchiveActivity: AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val (space, isNewSpace) = intent.extras.getSpace(Space.Type.INTERNET_ARCHIVE) + setContent { - InternetArchiveLoginScreen() + if (isNewSpace) { + InternetArchiveLoginScreen(space) + } else { + InternetArchiveScreen(space) + } } } } 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 new file mode 100644 index 00000000..038697eb --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt @@ -0,0 +1,54 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ARG_SPACE +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ARG_VAL_NEW_SPACE +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace +import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveFragment + +@Deprecated("only used for backward compatibility") +class InternetArchiveFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + val (space, isNewSpace) = arguments.getSpace(Space.Type.INTERNET_ARCHIVE) + + return ComposeView(requireContext()).apply { + setContent { + if (isNewSpace) { + InternetArchiveLoginScreen(space) + } else { + InternetArchiveScreen(space) + } + } + } + } + + companion object { + + const val RESP_SAVED = "ia_fragment_resp_saved" + const val RESP_DELETED = "ia_dav_fragment_resp_deleted" + const val RESP_CANCEL = "ia_fragment_resp_cancel" + + @JvmStatic + fun newInstance(spaceId: Long) = InternetArchiveFragment().apply { + arguments = Bundle().apply { + putLong(ARG_SPACE, spaceId) + } + } + + @JvmStatic + fun newInstance() = newInstance(ARG_VAL_NEW_SPACE) + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt new file mode 100644 index 00000000..9b279ce1 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt @@ -0,0 +1,60 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.state.Dispatch +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf + +@Composable +fun InternetArchiveScreen(space: Space) { + val viewModel: InternetArchiveViewModel = koinViewModel { + parametersOf(space) + } + + val state by viewModel.state.collectAsState() + + InternetArchiveContent(state, viewModel::dispatch) +} + +@Composable +private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispatch) { + Box(modifier = Modifier.fillMaxSize()) { + Column { + Text( + text = stringResource(id = R.string.prompt_email), + style = MaterialTheme.typography.caption + ) + Text( + text = state.email, + ) + Text( + text = "Username", + style = MaterialTheme.typography.caption + ) + Text( + text = state.username + ) + + Text( + text = "Expires", + style = MaterialTheme.typography.caption + ) + + Text( + text = state.expires + ) + } + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt new file mode 100644 index 00000000..7c0e44cf --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt @@ -0,0 +1,7 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation + +data class InternetArchiveState( + val username: String = "", + val email: String = "", + val expires: String= "", +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt new file mode 100644 index 00000000..3cf41cfa --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt @@ -0,0 +1,23 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation + +import net.opendasharchive.openarchive.core.presentation.StatefulViewModel +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action + +class InternetArchiveViewModel(private val space: Space) : + StatefulViewModel(InternetArchiveState()) { + + override fun reduce(state: InternetArchiveState, action: Action) = when (action) { + else -> state + } + + override suspend fun effects(state: InternetArchiveState, action: Action) { + when (action) { + else -> Unit + } + } + + sealed interface Action { + + } +} 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 e4084bd2..ff6c4376 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 @@ -1,6 +1,5 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login -import android.app.Activity import android.content.Intent import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult @@ -21,8 +20,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -32,17 +29,20 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.state.Dispatch +import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf @Composable -fun InternetArchiveLoginScreen() { - val viewModel: InternetArchiveLoginViewModel = koinViewModel() +fun InternetArchiveLoginScreen(space: Space) { + val viewModel: InternetArchiveLoginViewModel = koinViewModel { + parametersOf(space) + } val state by viewModel.state.collectAsState() @@ -78,12 +78,6 @@ private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispat horizontalAlignment = Alignment.CenterHorizontally ) { - state.auth?.let { auth -> - Text(text = "${auth.access}:${auth.secret}", - modifier = Modifier.padding(bottom = 20.dp), - color = Color.Red) - } - Text( text = stringResource(id = R.string.internet_archive), fontSize = 32.sp, @@ -132,7 +126,9 @@ private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispat Text(stringResource(id = R.string.title_activity_login)) } - TextButton(onClick = { dispatch(CreateLogin) }) { + TextButton( + modifier = Modifier.padding(top = 10.dp), + onClick = { dispatch(CreateLogin) }) { Text("Create Login") } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt index 767e2199..e1119507 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt @@ -1,6 +1,7 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import android.os.Bundle +import net.opendasharchive.openarchive.db.Space data class InternetArchiveLoginState( val email: String = "", @@ -8,5 +9,18 @@ data class InternetArchiveLoginState( val isEmailError: Boolean = false, val isPasswordError: Boolean = false, val isLoginError: Boolean = false, - val auth: InternetArchiveAuth? = null ) + +const val ARG_VAL_NEW_SPACE = -1L +const val ARG_SPACE = "space" +fun Bundle?.getSpace(type: Space.Type): Pair { + val mSpaceId = this?.getLong(ARG_SPACE, ARG_VAL_NEW_SPACE) ?: ARG_VAL_NEW_SPACE + + val isNewSpace = ARG_VAL_NEW_SPACE == mSpaceId + + return if (isNewSpace) { + Pair(Space(type), true) + } else { + Space.get(mSpaceId)?.let { Pair(it, false) } ?: Pair(Space(type), true) + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt index 41006cd1..14e7ab76 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -1,14 +1,12 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import net.opendasharchive.openarchive.core.state.StateDispatcher -import net.opendasharchive.openarchive.core.state.StateListener -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.core.presentation.StatefulViewModel +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginError @@ -17,55 +15,51 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.log import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword class InternetArchiveLoginViewModel( - private val repository: InternetArchiveRepository -) : ViewModel() { - private val dispatcher = StateDispatcher(InternetArchiveLoginState(), ::reduce, ::effects) - private val listener = StateListener() + private val repository: InternetArchiveRepository, + private val space: Space, +) : StatefulViewModel(InternetArchiveLoginState()) { - val state = dispatcher.state - val effects = listener.actions - - private fun reduce(state: InternetArchiveLoginState, action: Action): InternetArchiveLoginState = when(action) { + override fun reduce( + state: InternetArchiveLoginState, + action: Action + ): InternetArchiveLoginState = when (action) { is UpdateEmail -> state.copy(email = action.value) is UpdatePassword -> state.copy(password = action.value) is LoginError -> state.copy(isLoginError = true) - is LoginSuccess -> state.copy(auth = action.value) else -> state } - private suspend fun effects(state: InternetArchiveLoginState, action: Action) { - when(action) { + override suspend fun effects(state: InternetArchiveLoginState, action: Action) { + when (action) { is Login -> withContext(Dispatchers.IO) { repository.login(state.email, state.password) .onSuccess { - dispatcher.dispatch(LoginSuccess(it)) + space.username = it.auth.access + space.password = it.auth.secret + space.save() + send(LoginSuccess(it)) }.onFailure { - dispatcher.dispatch(LoginError(it)) + dispatch(LoginError(it)) } } - is CreateLogin -> listener.send(action) - else -> Unit - } - } - fun dispatch(action: Action) { - viewModelScope.launch { - dispatcher.dispatch(action) + is CreateLogin -> send(action) + else -> Unit } } sealed interface Action { - data object Login: Action + data object Login : Action - data class LoginSuccess(val value: InternetArchiveAuth): Action + data class LoginSuccess(val value: InternetArchive) : Action - data class LoginError(val value: Throwable): Action + data class LoginError(val value: Throwable) : Action - data object CreateLogin: Action { + data object CreateLogin : Action { const val URI = "https://archive.org/account/signup" } - data class UpdateEmail(val value: String): Action - data class UpdatePassword(val value: String): Action + data class UpdateEmail(val value: String) : Action + data class UpdatePassword(val value: String) : Action } } 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 9e97272c..34e4aefc 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 @@ -32,7 +32,6 @@ import net.opendasharchive.openarchive.db.Project import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.features.folders.AddFolderActivity -import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveActivity import net.opendasharchive.openarchive.features.media.AddMediaDialogFragment import net.opendasharchive.openarchive.features.media.Picker import net.opendasharchive.openarchive.features.media.PreviewActivity @@ -459,9 +458,7 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener mBinding.spacesCard.hide() } - //startActivity(Intent(this, SpaceSetupActivity::class.java)) - - startActivity(Intent(this, InternetArchiveActivity::class.java)) + startActivity(Intent(this, SpaceSetupActivity::class.java)) } override fun getSelectedSpace(): Space? { 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 16336697..fd11b2e0 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 @@ -10,7 +10,7 @@ import net.opendasharchive.openarchive.features.settings.SpaceSetupFragment import net.opendasharchive.openarchive.features.settings.SpaceSetupSuccessFragment import net.opendasharchive.openarchive.services.dropbox.DropboxFragment import net.opendasharchive.openarchive.services.gdrive.GDriveFragment -import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveFragment +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveFragment import net.opendasharchive.openarchive.services.internetarchive.Util import net.opendasharchive.openarchive.services.webdav.WebDavFragment @@ -242,4 +242,4 @@ class SpaceSetupActivity : BaseActivity() { Util.setBackgroundTint(mBinding.progressBlock.bar2, R.color.colorSpaceSetupProgressOn) Util.setBackgroundTint(mBinding.progressBlock.dot3, R.color.colorSpaceSetupProgressOn) } -} \ No newline at end of file +} From 90dca8619b73123fc96c7dc063278c7caae608ab Mon Sep 17 00:00:00 2001 From: RJ Date: Tue, 5 Mar 2024 13:25:30 -0800 Subject: [PATCH 25/63] fix(settings): use availability to check for setting toggle (#572) prompt user to add pin/biometrics if available --- .../settings/ProofModeSettingsActivity.kt | 64 +++++++++++++------ .../opendasharchive/openarchive/util/Hbks.kt | 50 ++++++++++++++- 2 files changed, 93 insertions(+), 21 deletions(-) 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 8822b3a0..6b781930 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 @@ -9,12 +9,16 @@ import android.os.Bundle import android.provider.Settings import android.view.MenuItem import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat import com.permissionx.guolindev.PermissionX +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.databinding.ActivitySettingsContainerBinding import net.opendasharchive.openarchive.features.core.BaseActivity @@ -31,6 +35,14 @@ class ProofModeSettingsActivity: BaseActivity() { class Fragment: PreferenceFragmentCompat() { + private val enrollBiometrics = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + findPreference(Prefs.USE_PROOFMODE_KEY_ENCRYPTION)?.let { + MainScope().launch { + enableProofModeKeyEncryption(it) + } + } + } + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.prefs_proof_mode, rootKey) @@ -67,8 +79,9 @@ class ProofModeSettingsActivity: BaseActivity() { val pkePreference = findPreference(Prefs.USE_PROOFMODE_KEY_ENCRYPTION) val activity = activity + val availability = Hbks.deviceAvailablity(requireContext()) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && activity != null && Hbks.deviceSecured(activity)) { + if (activity != null && availability !is Hbks.Availability.Unavailable) { pkePreference?.isSingleLineTitle = false pkePreference?.setTitle(when (Hbks.biometryType(activity)) { @@ -81,23 +94,10 @@ class ProofModeSettingsActivity: BaseActivity() { pkePreference?.setOnPreferenceChangeListener { _, newValue -> if (newValue as Boolean) { - val key = Hbks.loadKey() ?: Hbks.createKey() - - if (key != null && Prefs.proofModeEncryptedPassphrase == null) { - createPassphrase(key, activity) { - if (it != null) { - ProofModeHelper.removePgpKey(activity) - - // We need to kill the app and restart, - // since the ProofMode singleton loads the passphrase - // in its singleton constructor. Urgh. - ProofModeHelper.restartApp(activity) - } else { - Hbks.removeKey() - - pkePreference.isChecked = false - } - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && availability is Hbks.Availability.Enroll) { + enrollBiometrics.launch(Hbks.enrollIntent(availability.type)) + } else { + enableProofModeKeyEncryption(pkePreference) } } else { @@ -117,6 +117,34 @@ class ProofModeSettingsActivity: BaseActivity() { pkePreference?.isVisible = false } } + + private fun enableProofModeKeyEncryption(pkePreference: SwitchPreferenceCompat) { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return + } + + val key = Hbks.loadKey() ?: Hbks.createKey() + + if (key != null && Prefs.proofModeEncryptedPassphrase == null) { + createPassphrase(key, activity) { + if (it != null) { + ProofModeHelper.removePgpKey(requireContext()) + + // We need to kill the app and restart, + // since the ProofMode singleton loads the passphrase + // in its singleton constructor. Urgh. + ProofModeHelper.restartApp(requireActivity()) + } else { + Hbks.removeKey() + + pkePreference.isChecked = false + } + } + } else { + // What?? shouldn't happen if enrolled with a PIN or Fingerprint + } + } } private lateinit var mBinding: ActivitySettingsContainerBinding diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/Hbks.kt b/app/src/main/java/net/opendasharchive/openarchive/util/Hbks.kt index 76eb73c2..855d47dc 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/util/Hbks.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/util/Hbks.kt @@ -1,7 +1,10 @@ package net.opendasharchive.openarchive.util import android.content.Context +import android.content.Intent import android.os.Build +import android.provider.Settings.ACTION_BIOMETRIC_ENROLL +import android.provider.Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.security.keystore.UserNotAuthenticatedException @@ -29,8 +32,16 @@ object Hbks { fun canAuthenticate(manager: BiometricManager): Boolean { return manager.canAuthenticate(value) == BiometricManager.BIOMETRIC_SUCCESS } - } + fun canEnroll(manager: BiometricManager): Boolean { + return manager.canAuthenticate(value) == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + } + } + sealed interface Availability { + data class Available(val type: BiometryType) : Availability + @RequiresApi(Build.VERSION_CODES.R) data class Enroll(val type: BiometryType): Availability + data object Unavailable : Availability + } private const val alias = "save-main-key" private const val type = "AndroidKeyStore" @@ -232,8 +243,41 @@ object Hbks { return output } - fun deviceSecured(context: Context): Boolean { - return biometryType(context) != BiometryType.None + fun deviceAvailablity(context: Context): Availability { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return Availability.Unavailable + } + + val manager = BiometricManager.from(context) + + for (type in arrayOf(BiometryType.Both, BiometryType.StrongBiometry, BiometryType.DeviceCredential)) { + if (type.canAuthenticate(manager)) { + return Availability.Available(type) + } + } + + // enrolling only available in version greater than R + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + for (type in arrayOf( + BiometryType.Both, + BiometryType.StrongBiometry, + BiometryType.DeviceCredential + )) { + if (type.canEnroll(manager)) { + return Availability.Enroll(type) + } + } + } + + return Availability.Unavailable + } + + @RequiresApi(Build.VERSION_CODES.R) + fun enrollIntent(type: BiometryType) = Intent(ACTION_BIOMETRIC_ENROLL).apply { + putExtra( + EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, + type.value + ) } fun biometryType(context: Context): BiometryType { From d38dceb08f255dc6ca6a3d6be577dc7a7a7e32e4 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 5 Mar 2024 15:17:10 -0800 Subject: [PATCH 26/63] fix(fastlane): add versioning plugin and ability to do a manual release build --- Gemfile | 10 ++ Gemfile.lock | 222 ++++++++++++++++++++++++++++++++++++++++++++ fastlane/Fastfile | 70 ++++++++++---- fastlane/Pluginfile | 1 + 4 files changed, 283 insertions(+), 20 deletions(-) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 fastlane/Pluginfile diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..b734015f --- /dev/null +++ b/Gemfile @@ -0,0 +1,10 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +source "https://rubygems.org" + +gem 'fastlane' + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..e9b03fb3 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,222 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.895.0) + aws-sdk-core (3.191.3) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.77.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.143.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.109.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.3.0) + fastlane (2.219.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-android_version_manager (0.4.1) + semantic (~> 1.6.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.7.1) + jwt (2.8.1) + base64 + mini_magick (4.12.0) + mini_mime (1.1.5) + multi_json (1.15.0) + multipart-post (2.4.0) + nanaimo (0.3.0) + naturally (2.2.1) + nkf (0.2.0) + optparse (0.4.0) + os (1.1.4) + plist (3.7.1) + public_suffix (5.0.4) + rake (13.1.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.6) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + semantic (1.6.1) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.5.0) + word_wrap (1.0.0) + xcodeproj (1.24.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + fastlane + fastlane-plugin-android_version_manager + +BUNDLED WITH + 2.5.6 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ab92687c..b3938036 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,30 +6,64 @@ platform :android do gradle(task: "test") end + desc "Create a release build for manual deployment" + lane :release do + gradle( + task: "assemble", + build_type: "release", + properties: { + "android.injected.signing.store.file" => ENV["FASTLANE_KEYSTORE_FILE"], + "android.injected.signing.store.password" => ENV["FASTLANE_KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => ENV["FASTLANE_KEY_ALIAS"], + "android.injected.signing.key.password" => ENV["FASTLANE_KEY_PASSWORD"], + } + ) + + send_progress_message("Copying APK to current folder") + copy_artifacts( + artifacts: [lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]], + target_path: ENV["PWD"] + ) + end + + desc "Increments the version code" + lane :increment_version_code do + android_increment_version_code( + app_project_dir: "./app" + ) + end + + desc "Increments the version name" + lane :increment_version_name do + android_increment_version_name( + app_project_dir: "./app" + ) + end + desc "Submit a new Internal Build" lane :internal do - send_progress_message("Build Started :rocket:") + send_progress_message("Build Started") gradle(task: "clean assembleRelease") - send_progress_message("Uploading To Internal track :rocket:") + send_progress_message("Uploading To Internal track") upload_to_play_store(track: "internal") end desc "Submit a new Alpha Build" lane :alpha do - send_progress_message("Build Started :rocket:") + send_progress_message("Build Started") gradle(task: "clean assembleRelease") - send_progress_message("Uploading To Alpha track :rocket:") + send_progress_message("Uploading To Alpha track") upload_to_play_store(track: "alpha") end desc "Submit a new Beta Build" lane :beta do - send_progress_message("Build Started :rocket:") + send_progress_message("Build Started 🚀") gradle(task: "clean assembleRelease") - send_progress_message("Uploading To Beta track :rocket:") + send_progress_message("Uploading To Beta track") upload_to_play_store(track: "beta") end @@ -53,18 +87,14 @@ def on_error(exception) end after_all do |lane| - file_name = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH].gsub(/\/.*\//,"") - send_message "Successfully deployed new App Update! :champagne:" - default_payloads = [ - :git_branch, - :last_git_commit_hash, - :last_git_commit_message - ] - payload = { - "Build Date" => Time.new.to_s, - "APK" => file_name - } - send_message file_name - send_message "#{default_payloads}" - send_message "#{payload}" + if lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH] + file_name = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH].gsub(/\/.*\//,"") + send_message "Successfully deployed new App Update!" + payload = { + "Build Date" => Time.new.to_s, + "APK" => file_name + } + send_message file_name + send_message "#{payload}" + end end diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile new file mode 100644 index 00000000..2866db63 --- /dev/null +++ b/fastlane/Pluginfile @@ -0,0 +1 @@ +gem 'fastlane-plugin-android_version_manager' From 1a666279898f245a52a87c6374ef1b921ef7cac5 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 5 Mar 2024 18:24:23 -0800 Subject: [PATCH 27/63] fix(ia): add result handlers, styling, alerts, strings --- .../presentation/InternetArchiveActivity.kt | 19 ++- .../presentation/InternetArchiveFragment.kt | 15 ++- .../presentation/InternetArchiveScreen.kt | 112 +++++++++++++++++- .../presentation/InternetArchiveViewModel.kt | 12 +- .../components/InternetArchiveHeader.kt | 62 ++++++++++ .../login/InternetArchiveLoginScreen.kt | 109 +++++++++++++---- .../login/InternetArchiveLoginViewModel.kt | 8 +- app/src/main/res/values/strings.xml | 1 + 8 files changed, 300 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt 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 a23b9471..a8c98414 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,13 +1,13 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation +import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace -import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveFragment.Companion.ARG_VAL_NEW_SPACE +import net.opendasharchive.openarchive.features.main.MainActivity class InternetArchiveActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -16,10 +16,21 @@ class InternetArchiveActivity: AppCompatActivity() { setContent { if (isNewSpace) { - InternetArchiveLoginScreen(space) + InternetArchiveLoginScreen(space) { + finish(it) + } } else { - InternetArchiveScreen(space) + InternetArchiveScreen(space) { + finish(it) + } } } } + + private fun finish(result: String) { + when(result) { + RESP_SAVED -> startActivity(Intent(this, MainActivity::class.java)) + RESP_CANCEL -> Space.navigate(this) + } + } } 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 038697eb..826ea755 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 @@ -5,13 +5,18 @@ import android.view.LayoutInflater 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.login.ARG_SPACE import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ARG_VAL_NEW_SPACE import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace -import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveFragment + +const val RESP_SAVED = "ia_fragment_resp_saved" +const val RESP_DELETED = "ia_dav_fragment_resp_deleted" +const val RESP_CANCEL = "ia_fragment_resp_cancel" @Deprecated("only used for backward compatibility") class InternetArchiveFragment : Fragment() { @@ -27,9 +32,13 @@ class InternetArchiveFragment : Fragment() { return ComposeView(requireContext()).apply { setContent { if (isNewSpace) { - InternetArchiveLoginScreen(space) + InternetArchiveLoginScreen(space) { result -> + setFragmentResult(result, bundleOf()) + } } else { - InternetArchiveScreen(space) + InternetArchiveScreen(space) { result -> + setFragmentResult(result, bundleOf()) + } } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt index 9b279ce1..8d74aff7 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt @@ -3,58 +3,162 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton import androidx.compose.material.Text 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.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf +import java.util.Date @Composable -fun InternetArchiveScreen(space: Space) { +fun InternetArchiveScreen(space: Space, onResult: (String) -> Unit) { val viewModel: InternetArchiveViewModel = koinViewModel { parametersOf(space) } val state by viewModel.state.collectAsState() + LaunchedEffect(Unit) { + viewModel.effects.collect { action -> + when (action) { + is Action.Remove -> { + onResult(RESP_DELETED) + } + is Action.Cancel -> { + onResult(RESP_CANCEL) + } + } + } + } + InternetArchiveContent(state, viewModel::dispatch) } @Composable private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispatch) { - Box(modifier = Modifier.fillMaxSize()) { + + var isRemoving: Boolean by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + ) { + Column { + + InternetArchiveHeader() + Text( + modifier = Modifier.padding(top = 12.dp), text = stringResource(id = R.string.prompt_email), style = MaterialTheme.typography.caption ) Text( text = state.email, + fontSize = 18.sp ) Text( + modifier = Modifier.padding(top = 12.dp), text = "Username", style = MaterialTheme.typography.caption ) Text( - text = state.username + text = state.username, + fontSize = 18.sp ) Text( + modifier = Modifier.padding(top = 12.dp), text = "Expires", style = MaterialTheme.typography.caption ) Text( - text = state.expires + text = state.expires, + fontSize = 18.sp ) + + Button( + modifier = Modifier + .padding(top = 12.dp) + .align(Alignment.CenterHorizontally), + onClick = { + isRemoving = true + }, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error) + ) { + Text(stringResource(id = R.string.menu_delete)) + } } } + + if (isRemoving) { + RemoveInternetArchiveDialog(isRemoving = isRemoving, onDismiss = { isRemoving = false }) { + dispatch(Action.Remove) + } + } +} + +@Composable +private fun RemoveInternetArchiveDialog(isRemoving: Boolean, onDismiss: () -> Unit, onRemove: () -> Unit) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(id = R.string.remove_from_app))}, + text = { Text(stringResource(id = R.string.are_you_sure_you_want_to_remove_this_server_from_the_app))}, + dismissButton = { + OutlinedButton( + onClick = onDismiss + ) { + Text(stringResource(id = R.string.action_cancel)) + } + }, confirmButton = { + Button( + onClick = onRemove, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error) + ) { + Text(stringResource(id = R.string.remove)) + } + }) +} + +@Composable +@Preview(showBackground = true) +private fun InternetArchiveScreenPreview() { + InternetArchiveContent( + state = InternetArchiveState( + email = "abc@example.com", + username = "@abc_name", + expires = Date().toString() + ) + ) {} +} + +@Composable +@Preview(showBackground = true) +private fun RemoveInternetArchiveDialogPreview() { + RemoveInternetArchiveDialog(isRemoving = true, onDismiss = { }) { + } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt index 3cf41cfa..3e3d906b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt @@ -7,17 +7,21 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.Int class InternetArchiveViewModel(private val space: Space) : StatefulViewModel(InternetArchiveState()) { - override fun reduce(state: InternetArchiveState, action: Action) = when (action) { - else -> state - } + override fun reduce(state: InternetArchiveState, action: Action) = state override suspend fun effects(state: InternetArchiveState, action: Action) { when (action) { - else -> Unit + is Action.Remove -> { + space.delete() + send(action) + } + is Action.Cancel -> send(action) } } sealed interface Action { + data object Remove : Action + data object Cancel : Action } } 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 new file mode 100644 index 00000000..2c6ddb67 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt @@ -0,0 +1,62 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 + +@Composable +fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 18.sp) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier + .size(48.dp) + .background( + color = colorResource(id = R.color.colorBackgroundSpaceIcon), + shape = CircleShape + ).clip(CircleShape)) { + Image( + modifier = Modifier.matchParentSize().padding(12.dp), + painter = painterResource(id = R.drawable.ic_internet_archive), + contentDescription = stringResource( + id = R.string.internet_archive + ), + colorFilter = tint(colorResource(id = R.color.colorPrimary)) + ) + } + Column(modifier = Modifier.padding(start = 8.dp)) { + Text( + text = stringResource(id = R.string.internet_archive), + fontSize = titleSize, + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(id = R.string.internet_archive_description) + ) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun InternetArchiveHeaderPreview() { + InternetArchiveHeader() +} 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 ff6c4376..117fc351 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 @@ -4,32 +4,47 @@ import android.content.Intent import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +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 import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource 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.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.presentation.RESP_CANCEL +import net.opendasharchive.openarchive.features.internetarchive.presentation.RESP_SAVED +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login @@ -39,7 +54,7 @@ import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @Composable -fun InternetArchiveLoginScreen(space: Space) { +fun InternetArchiveLoginScreen(space: Space, onResult: (String) -> Unit) { val viewModel: InternetArchiveLoginViewModel = koinViewModel { parametersOf(space) } @@ -61,6 +76,10 @@ fun InternetArchiveLoginScreen(space: Space) { ) ) + is Action.Cancel -> onResult(RESP_CANCEL) + + is Action.LoginSuccess -> onResult(RESP_SAVED) + else -> Unit } } @@ -70,38 +89,54 @@ fun InternetArchiveLoginScreen(space: Space) { } @Composable -private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispatch: Dispatch) { +private fun InternetArchiveLoginContent( + state: InternetArchiveLoginState, + dispatch: Dispatch +) { + + LaunchedEffect(state.isLoginError) { + while (state.isLoginError) { + delay(3000) + dispatch(Action.ErrorFade) + } + } - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + ) { Column( Modifier.align(Alignment.Center), horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = stringResource(id = R.string.internet_archive), - fontSize = 32.sp, - modifier = Modifier.padding(bottom = 20.dp) + InternetArchiveHeader( + modifier = Modifier.padding(bottom = 24.dp), + titleSize = 32.sp ) TextField( value = state.email, onValueChange = { dispatch(UpdateEmail(it)) }, label = { Text(stringResource(id = R.string.prompt_email)) }, - placeholder = { Text(stringResource(id = R.string.prompt_email)) }, singleLine = true, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next, autoCorrect = false, keyboardType = KeyboardType.Email ), - isError = state.isEmailError + isError = state.isEmailError, + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = colorResource(id = R.color.colorPrimary) + ) ) + Spacer(Modifier.height(12.dp)) + TextField( value = state.password, onValueChange = { dispatch(UpdatePassword(it)) }, label = { Text(stringResource(id = R.string.prompt_password)) }, - placeholder = { Text(stringResource(id = R.string.prompt_password)) }, singleLine = true, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions( @@ -109,10 +144,13 @@ private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispat autoCorrect = false, imeAction = ImeAction.Go ), - isError = state.isPasswordError + isError = state.isPasswordError, + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = colorResource(id = R.color.colorPrimary) + ) ) - if (state.isLoginError) { + AnimatedVisibility(visible = state.isLoginError) { Text( modifier = Modifier.padding(top = 20.dp), text = stringResource(id = R.string.error_incorrect_username_or_password), @@ -120,28 +158,57 @@ private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispat ) } - Button( - modifier = Modifier.padding(top = 20.dp), - onClick = { dispatch(Login) }) { - Text(stringResource(id = R.string.title_activity_login)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround + ) { + OutlinedButton( + colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colors.onSurface), + onClick = { dispatch(Action.Cancel) } + ) { + Text(stringResource(id = R.string.action_cancel)) + } + Button( + onClick = { dispatch(Login) }, + colors = ButtonDefaults.buttonColors( + backgroundColor = colorResource(id = R.color.colorPrimary), + contentColor = colorResource(id = R.color.colorBackground) + ) + ) { + Text( + text = stringResource(id = R.string.title_activity_login), + fontSize = 18.sp, + ) + } } - TextButton( + Row( modifier = Modifier.padding(top = 10.dp), - onClick = { dispatch(CreateLogin) }) { - Text("Create Login") + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "No account?" + ) + TextButton( + colors = ButtonDefaults.textButtonColors(contentColor = colorResource(id = R.color.colorPrimary)), + onClick = { dispatch(CreateLogin) }) { + Text(text = "Create Login", fontSize = 16.sp, fontWeight = FontWeight.Black) + } } } } } @Composable -@Preview +@Preview(showBackground = true) private fun InternetArchiveLoginPreview() { InternetArchiveLoginContent( state = InternetArchiveLoginState( email = "user@example.org", - password = "123abc" + password = "abc123" ) ) {} } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt index 14e7ab76..bbe08a47 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -26,6 +26,7 @@ class InternetArchiveLoginViewModel( is UpdateEmail -> state.copy(email = action.value) is UpdatePassword -> state.copy(password = action.value) is LoginError -> state.copy(isLoginError = true) + is Action.ErrorFade -> state.copy(isLoginError = false) else -> state } @@ -42,8 +43,7 @@ class InternetArchiveLoginViewModel( dispatch(LoginError(it)) } } - - is CreateLogin -> send(action) + is CreateLogin, is Action.Cancel -> send(action) else -> Unit } } @@ -51,10 +51,14 @@ class InternetArchiveLoginViewModel( sealed interface Action { data object Login : Action + data object Cancel : Action + data class LoginSuccess(val value: InternetArchive) : Action data class LoginError(val value: Throwable) : Action + data object ErrorFade : Action + data object CreateLogin : Action { const val URI = "https://archive.org/account/signup" } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6813a14a..a46ca1d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,7 @@ Internet Archive + Upload and preserve media in a digital library Google Drive Google Drive™ Upload to Google Drive From 3bc577507de6e0b7b4acce7662c5d580c3efdc6d Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 5 Mar 2024 18:34:53 -0800 Subject: [PATCH 28/63] fix(ia): login text field colors --- .../login/InternetArchiveLoginScreen.kt | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) 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 117fc351..cd8a475a 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 @@ -119,7 +119,12 @@ private fun InternetArchiveLoginContent( TextField( value = state.email, onValueChange = { dispatch(UpdateEmail(it)) }, - label = { Text(stringResource(id = R.string.prompt_email)) }, + label = { + Text( + text = stringResource(id = R.string.prompt_email), + color = colorResource(id = R.color.colorPrimary) + ) + }, singleLine = true, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next, @@ -127,16 +132,25 @@ private fun InternetArchiveLoginContent( keyboardType = KeyboardType.Email ), isError = state.isEmailError, - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = colorResource(id = R.color.colorPrimary) - ) + colors = colorResource(id = R.color.colorPrimary).let { + TextFieldDefaults.textFieldColors( + focusedIndicatorColor = it, + focusedLabelColor = it, + cursorColor = it + ) + } ) Spacer(Modifier.height(12.dp)) TextField( value = state.password, onValueChange = { dispatch(UpdatePassword(it)) }, - label = { Text(stringResource(id = R.string.prompt_password)) }, + label = { + Text( + stringResource(id = R.string.prompt_password), + color = colorResource(id = R.color.colorPrimary) + ) + }, singleLine = true, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions( @@ -145,9 +159,13 @@ private fun InternetArchiveLoginContent( imeAction = ImeAction.Go ), isError = state.isPasswordError, - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = colorResource(id = R.color.colorPrimary) - ) + colors = colorResource(id = R.color.colorPrimary).let { + TextFieldDefaults.textFieldColors( + focusedIndicatorColor = it, + focusedLabelColor = it, + cursorColor = it + ) + } ) AnimatedVisibility(visible = state.isLoginError) { From 3fca8d38b9c4f1531af7e71e867d1123b814665e Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 5 Mar 2024 18:35:52 -0800 Subject: [PATCH 29/63] fix(main): ensure settings in adapter is always in line with button order --- .../opendasharchive/openarchive/features/main/ProjectAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt index 35e1d99c..8906074f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt @@ -19,7 +19,7 @@ class ProjectAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : } val settingsIndex: Int - get() = projects.size + get() = max(1, projects.size) fun updateData(projects: List) { this.projects = projects From bb2803a4f8fa1f5e56d53f25e08969634d0e5a0c Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Wed, 6 Mar 2024 00:29:45 -0800 Subject: [PATCH 30/63] fix(ia): move login logic to use case with testing the connection consolidated bundles, activity and fragment code. should ultimately be not necessary. added fadein/out of login errors use a spinner and disable text fields while logging in --- .../openarchive/core/state/StateDispatcher.kt | 3 +- .../features/internetarchive/Module.kt | 11 +++++- .../usecase/InternetArchiveLoginUseCase.kt | 13 ++++++- .../datasource/InternetArchiveRemoteSource.kt | 8 ++-- .../mapping/InternetArchiveMapper.kt | 6 +-- .../model/UnauthenticatedException.kt | 3 ++ .../repository/InternetArchiveRepository.kt | 16 +++++--- .../presentation/InternetArchiveActivity.kt | 30 +++++++++++--- .../presentation/InternetArchiveFragment.kt | 39 +++++++++++-------- .../presentation/InternetArchiveScreen.kt | 18 ++++----- .../presentation/InternetArchiveState.kt | 1 + .../presentation/components/BundleExt.kt | 38 ++++++++++++++++++ .../login/InternetArchiveLoginScreen.kt | 37 ++++++++++++------ .../login/InternetArchiveLoginState.kt | 18 +-------- .../login/InternetArchiveLoginViewModel.kt | 38 ++++++++++-------- gradle.properties | 1 - 16 files changed, 182 insertions(+), 98 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/UnauthenticatedException.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/BundleExt.kt diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt index a7e0758d..2aac0d91 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt @@ -3,6 +3,7 @@ package net.opendasharchive.openarchive.core.state import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch @@ -18,7 +19,7 @@ class StateDispatcher( private val effects: Effect ) { private val _state = MutableStateFlow(initialState) - val state = _state + val state = _state.asStateFlow() fun dispatch(action: A) { val state = _state.updateAndGet { reducer(it, action) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index 4b8d3451..1eea9aaa 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -1,5 +1,7 @@ package net.opendasharchive.openarchive.features.internetarchive +import com.google.gson.FieldNamingPolicy +import com.google.gson.Gson import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository @@ -8,8 +10,13 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val internetArchiveModule = module { - factory { InternetArchiveRemoteSource(get()) } + single { + Gson().newBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create() + } + factory { InternetArchiveRemoteSource(get(), get()) } factory { InternetArchiveMapper() } - single { InternetArchiveRepository(get(), get()) } + factory { InternetArchiveRepository(get(), get()) } viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt index 0315feea..e2c9bc1c 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt @@ -1,4 +1,15 @@ package net.opendasharchive.openarchive.features.internetarchive.domain.usecase -class InternetArchiveLoginUseCase { +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository + +class InternetArchiveLoginUseCase( + private val repository: InternetArchiveRepository +) { + + suspend operator fun invoke(email: String, password: String): Result = + repository.login(email, password).mapCatching { response -> + repository.testConnection(response.auth).getOrThrow() + response + } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt index 5f48e8fd..47f0bf26 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt @@ -11,14 +11,12 @@ import net.opendasharchive.openarchive.services.internetarchive.IaConduit.Compan import okhttp3.FormBody import okhttp3.Request -private val LOGIN_URI = "https://archive.org/services/xauthn?op=login" +private const val LOGIN_URI = "https://archive.org/services/xauthn?op=login" class InternetArchiveRemoteSource( - private val context: Context + private val context: Context, + private val gson: Gson ) { - - private val gson = Gson() - suspend fun login(request: InternetArchiveLoginRequest): Result = SaveClient.get(context).enqueueResult( Request.Builder() diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt index f3e4c407..95541e18 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt @@ -6,14 +6,14 @@ import net.opendasharchive.openarchive.features.internetarchive.infrastructure.m class InternetArchiveMapper { - private fun toAuth(response: InternetArchiveLoginResponse.S3) = InternetArchiveAuth( + private operator fun invoke(response: InternetArchiveLoginResponse.S3) = InternetArchiveAuth( access = response.access, secret = response.secret ) - fun toDomain(response: InternetArchiveLoginResponse.Values) = InternetArchive( + operator fun invoke(response: InternetArchiveLoginResponse.Values) = InternetArchive( username = response.screenname ?: response.itemname ?: "", email = response.email ?: "", expires = response.expires ?: "", - auth = response.s3?.let { toAuth(it) } ?: InternetArchiveAuth("", "") + auth = response.s3?.let { invoke(it) } ?: InternetArchiveAuth("", "") ) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/UnauthenticatedException.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/UnauthenticatedException.kt new file mode 100644 index 00000000..a803cb4b --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/UnauthenticatedException.kt @@ -0,0 +1,3 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.model + +class UnauthenticatedException : RuntimeException() diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt index 5dc2d6d7..41435d32 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt @@ -3,9 +3,11 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure. import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.UnauthenticatedException class InternetArchiveRepository( private val remoteSource: InternetArchiveRemoteSource, @@ -15,13 +17,17 @@ class InternetArchiveRepository( withContext(Dispatchers.IO) { remoteSource.login( InternetArchiveLoginRequest(email, password) - ).mapCatching { - if (it.success.not()) { - throw IllegalArgumentException(it.values.reason) + ).mapCatching { response -> + if (response.success.not()) { + throw IllegalArgumentException(response.values.reason) } - when(it.version) { - else -> mapper.toDomain(it.values) + when(response.version) { + else -> mapper(response.values) } } } + + suspend fun testConnection(auth: InternetArchiveAuth): Result = withContext(Dispatchers.IO) { + remoteSource.testConnection(auth).mapCatching { if (!it) throw UnauthenticatedException() } + } } 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 a8c98414..ff9137ec 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,15 +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 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.InternetArchiveLoginScreen -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace import net.opendasharchive.openarchive.features.main.MainActivity -class InternetArchiveActivity: AppCompatActivity() { +@Deprecated("use jetpack compose") +class InternetArchiveActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val (space, isNewSpace) = intent.extras.getSpace(Space.Type.INTERNET_ARCHIVE) @@ -27,10 +31,24 @@ class InternetArchiveActivity: AppCompatActivity() { } } - private fun finish(result: String) { - when(result) { - RESP_SAVED -> startActivity(Intent(this, MainActivity::class.java)) - RESP_CANCEL -> Space.navigate(this) + private fun finish(result: IAResult) { + when (result) { + IAResult.Saved -> { + startActivity(Intent(this, MainActivity::class.java)) + measureNewBackend(Space.Type.INTERNET_ARCHIVE) + } + IAResult.Cancelled -> Space.navigate(this) + else -> Unit } } } + +fun Activity.measureNewBackend(type: Space.Type) { + CleanInsightsManager.getConsent(this) { + CleanInsightsManager.measureEvent( + "backend", + "new", + type.friendlyName + ) + } +} 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 826ea755..2c7a11ba 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 @@ -9,14 +9,11 @@ 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.login.ARG_SPACE -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ARG_VAL_NEW_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.internetarchive.presentation.login.InternetArchiveLoginScreen -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace - -const val RESP_SAVED = "ia_fragment_resp_saved" -const val RESP_DELETED = "ia_dav_fragment_resp_deleted" -const val RESP_CANCEL = "ia_fragment_resp_cancel" @Deprecated("only used for backward compatibility") class InternetArchiveFragment : Fragment() { @@ -33,31 +30,39 @@ class InternetArchiveFragment : Fragment() { setContent { if (isNewSpace) { InternetArchiveLoginScreen(space) { result -> - setFragmentResult(result, bundleOf()) + finish(result) } } else { InternetArchiveScreen(space) { result -> - setFragmentResult(result, bundleOf()) + finish(result) } } } } } + private fun finish(result: IAResult) { + setFragmentResult(result.value, bundleOf()) + + if (result == IAResult.Saved) { + activity?.measureNewBackend(Space.Type.INTERNET_ARCHIVE) + } + } + companion object { - const val RESP_SAVED = "ia_fragment_resp_saved" - const val RESP_DELETED = "ia_dav_fragment_resp_deleted" - const val RESP_CANCEL = "ia_fragment_resp_cancel" + val RESP_SAVED = IAResult.Saved.value + val RESP_CANCEL = IAResult.Cancelled.value @JvmStatic - fun newInstance(spaceId: Long) = InternetArchiveFragment().apply { - arguments = Bundle().apply { - putLong(ARG_SPACE, spaceId) - } + fun newInstance(args: Bundle) = InternetArchiveFragment().apply { + arguments = args } @JvmStatic - fun newInstance() = newInstance(ARG_VAL_NEW_SPACE) + fun newInstance(spaceId: Long) = newInstance(args = bundleWithSpaceId(spaceId)) + + @JvmStatic + fun newInstance() = newInstance(args = bundleWithNewSpace()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt index 8d74aff7..7bab2d5d 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt @@ -27,13 +27,14 @@ import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf import java.util.Date @Composable -fun InternetArchiveScreen(space: Space, onResult: (String) -> Unit) { +fun InternetArchiveScreen(space: Space, onResult: (IAResult) -> Unit) { val viewModel: InternetArchiveViewModel = koinViewModel { parametersOf(space) } @@ -43,12 +44,8 @@ fun InternetArchiveScreen(space: Space, onResult: (String) -> Unit) { LaunchedEffect(Unit) { viewModel.effects.collect { action -> when (action) { - is Action.Remove -> { - onResult(RESP_DELETED) - } - is Action.Cancel -> { - onResult(RESP_CANCEL) - } + is Action.Remove -> onResult(IAResult.Deleted) + is Action.Cancel -> onResult(IAResult.Cancelled) } } } @@ -116,14 +113,14 @@ private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispat } if (isRemoving) { - RemoveInternetArchiveDialog(isRemoving = isRemoving, onDismiss = { isRemoving = false }) { + RemoveInternetArchiveDialog(onDismiss = { isRemoving = false }) { dispatch(Action.Remove) } } } @Composable -private fun RemoveInternetArchiveDialog(isRemoving: Boolean, onDismiss: () -> Unit, onRemove: () -> Unit) { +private fun RemoveInternetArchiveDialog(onDismiss: () -> Unit, onRemove: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, title = { Text(stringResource(id = R.string.remove_from_app))}, @@ -159,6 +156,5 @@ private fun InternetArchiveScreenPreview() { @Composable @Preview(showBackground = true) private fun RemoveInternetArchiveDialogPreview() { - RemoveInternetArchiveDialog(isRemoving = true, onDismiss = { }) { - } + RemoveInternetArchiveDialog(onDismiss = { }) {} } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt index 7c0e44cf..dcffbe1d 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt @@ -5,3 +5,4 @@ data class InternetArchiveState( val email: String = "", val expires: String= "", ) + diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/BundleExt.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/BundleExt.kt new file mode 100644 index 00000000..c2321afc --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/BundleExt.kt @@ -0,0 +1,38 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.components + +import android.os.Bundle +import androidx.core.os.bundleOf +import net.opendasharchive.openarchive.db.Space + +@Deprecated("only for use with fragments and activities") +private const val ARG_VAL_NEW_SPACE = -1L + +@Deprecated("only for use with fragments and activities") +private const val ARG_SPACE = "space" + +@Deprecated("only for use with fragments and activities") +enum class IAResult( + @Deprecated("only for use with fragments and activities") + val value: String +) { + Saved("ia_fragment_resp_saved"), Deleted("ia_fragment_resp_deleted"), Cancelled("ia_fragment_resp_cancel"), +} + +@Deprecated("only for use with fragments and activities") +fun bundleWithSpaceId(spaceId: Long) = bundleOf(ARG_SPACE to spaceId) + +@Deprecated("only for use with fragments and activities") +fun bundleWithNewSpace() = bundleOf(ARG_SPACE to ARG_VAL_NEW_SPACE) + +@Deprecated("only for use with fragments and activities") +fun Bundle?.getSpace(type: Space.Type): Pair { + val mSpaceId = this?.getLong(ARG_SPACE, ARG_VAL_NEW_SPACE) ?: ARG_VAL_NEW_SPACE + + val isNewSpace = ARG_VAL_NEW_SPACE == mSpaceId + + return if (isNewSpace) { + Pair(Space(type), true) + } else { + Space.get(mSpaceId)?.let { Pair(it, false) } ?: Pair(Space(type), true) + } +} 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 cd8a475a..f9ffbea9 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 @@ -5,6 +5,8 @@ import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts 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 @@ -17,6 +19,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedButton import androidx.compose.material.Text @@ -42,8 +45,7 @@ import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.internetarchive.presentation.RESP_CANCEL -import net.opendasharchive.openarchive.features.internetarchive.presentation.RESP_SAVED +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.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin @@ -54,7 +56,7 @@ import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @Composable -fun InternetArchiveLoginScreen(space: Space, onResult: (String) -> Unit) { +fun InternetArchiveLoginScreen(space: Space, onResult: (IAResult) -> Unit) { val viewModel: InternetArchiveLoginViewModel = koinViewModel { parametersOf(space) } @@ -76,9 +78,9 @@ fun InternetArchiveLoginScreen(space: Space, onResult: (String) -> Unit) { ) ) - is Action.Cancel -> onResult(RESP_CANCEL) + is Action.Cancel -> onResult(IAResult.Cancelled) - is Action.LoginSuccess -> onResult(RESP_SAVED) + is Action.LoginSuccess -> onResult(IAResult.Saved) else -> Unit } @@ -118,6 +120,7 @@ private fun InternetArchiveLoginContent( TextField( value = state.email, + enabled = !state.isBusy, onValueChange = { dispatch(UpdateEmail(it)) }, label = { Text( @@ -144,7 +147,9 @@ private fun InternetArchiveLoginContent( Spacer(Modifier.height(12.dp)) TextField( - value = state.password, onValueChange = { dispatch(UpdatePassword(it)) }, + value = state.password, + enabled = !state.isBusy, + onValueChange = { dispatch(UpdatePassword(it)) }, label = { Text( stringResource(id = R.string.prompt_password), @@ -168,9 +173,12 @@ private fun InternetArchiveLoginContent( } ) - AnimatedVisibility(visible = state.isLoginError) { + AnimatedVisibility( + modifier = Modifier.padding(top = 20.dp), + visible = state.isLoginError, + enter = fadeIn(), exit = fadeOut() + ) { Text( - modifier = Modifier.padding(top = 20.dp), text = stringResource(id = R.string.error_incorrect_username_or_password), color = MaterialTheme.colors.error ) @@ -190,16 +198,21 @@ private fun InternetArchiveLoginContent( Text(stringResource(id = R.string.action_cancel)) } Button( + enabled = !state.isBusy, onClick = { dispatch(Login) }, colors = ButtonDefaults.buttonColors( backgroundColor = colorResource(id = R.color.colorPrimary), contentColor = colorResource(id = R.color.colorBackground) ) ) { - Text( - text = stringResource(id = R.string.title_activity_login), - fontSize = 18.sp, - ) + if (state.isBusy) { + CircularProgressIndicator() + } else { + Text( + text = stringResource(id = R.string.title_activity_login), + fontSize = 18.sp, + ) + } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt index e1119507..08db583b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt @@ -1,26 +1,10 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login -import android.os.Bundle -import net.opendasharchive.openarchive.db.Space - data class InternetArchiveLoginState( val email: String = "", val password: String = "", val isEmailError: Boolean = false, val isPasswordError: Boolean = false, val isLoginError: Boolean = false, + val isBusy: Boolean = false ) - -const val ARG_VAL_NEW_SPACE = -1L -const val ARG_SPACE = "space" -fun Bundle?.getSpace(type: Space.Type): Pair { - val mSpaceId = this?.getLong(ARG_SPACE, ARG_VAL_NEW_SPACE) ?: ARG_VAL_NEW_SPACE - - val isNewSpace = ARG_VAL_NEW_SPACE == mSpaceId - - return if (isNewSpace) { - Pair(Space(type), true) - } else { - Space.get(mSpaceId)?.let { Pair(it, false) } ?: Pair(Space(type), true) - } -} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt index bbe08a47..7e2e530c 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -1,13 +1,13 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.core.presentation.StatefulViewModel import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive -import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository +import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.InternetArchiveLoginUseCase import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Cancel import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.ErrorFade import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginError import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess @@ -15,7 +15,7 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.log import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword class InternetArchiveLoginViewModel( - private val repository: InternetArchiveRepository, + private val loginUseCase: InternetArchiveLoginUseCase, private val space: Space, ) : StatefulViewModel(InternetArchiveLoginState()) { @@ -25,25 +25,24 @@ class InternetArchiveLoginViewModel( ): InternetArchiveLoginState = when (action) { is UpdateEmail -> state.copy(email = action.value) is UpdatePassword -> state.copy(password = action.value) - is LoginError -> state.copy(isLoginError = true) - is Action.ErrorFade -> state.copy(isLoginError = false) + is Login -> state.copy(isBusy = true) + is LoginError -> state.copy(isLoginError = true, isBusy = false) + is LoginSuccess, is Cancel -> state.copy(isBusy = false) + is ErrorFade -> state.copy(isLoginError = false) else -> state } override suspend fun effects(state: InternetArchiveLoginState, action: Action) { when (action) { - is Login -> withContext(Dispatchers.IO) { - repository.login(state.email, state.password) - .onSuccess { - space.username = it.auth.access - space.password = it.auth.secret - space.save() - send(LoginSuccess(it)) - }.onFailure { - dispatch(LoginError(it)) + is Login -> + loginUseCase(state.email, state.password) + .onSuccess { ia -> + space.saveAndSetCurrent() + send(LoginSuccess(ia)) } - } - is CreateLogin, is Action.Cancel -> send(action) + .onFailure { dispatch(LoginError(it)) } + + is CreateLogin, is Cancel -> send(action) else -> Unit } } @@ -66,4 +65,9 @@ class InternetArchiveLoginViewModel( data class UpdateEmail(val value: String) : Action data class UpdatePassword(val value: String) : Action } + + private fun Space.saveAndSetCurrent() { + save() + Space.current = this + } } diff --git a/gradle.properties b/gradle.properties index 82e420b5..f9916a37 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # org.gradle.parallel=true android.useAndroidX=true android.enableJetifier=true -#android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=true android.nonFinalResIds=true From 119dbe93060f6a13df73a05076b4b202f3dd2e4f Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Wed, 6 Mar 2024 10:18:22 -0800 Subject: [PATCH 31/63] fix(ia): add missing dependency definition for login use case --- app/build.gradle | 4 ++-- .../openarchive/features/internetarchive/Module.kt | 2 ++ .../openarchive/features/settings/SettingsFragment.kt | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a180a0ac..b27a7b6b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { applicationId "net.opendasharchive.openarchive" minSdkVersion 21 targetSdkVersion 34 - versionCode 20549 - versionName '0.3.1-alpha2' + versionCode 20552 + versionName '0.3.2' archivesBaseName = "Save-$versionName" multiDexEnabled true vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index 1eea9aaa..fc19e4ea 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -2,6 +2,7 @@ package net.opendasharchive.openarchive.features.internetarchive import com.google.gson.FieldNamingPolicy import com.google.gson.Gson +import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.InternetArchiveLoginUseCase import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository @@ -18,5 +19,6 @@ val internetArchiveModule = module { factory { InternetArchiveRemoteSource(get(), get()) } factory { InternetArchiveMapper() } factory { InternetArchiveRepository(get(), get()) } + factory { InternetArchiveLoginUseCase(get()) } viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } 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 02ebd64d..8c9acfe2 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 @@ -12,7 +12,7 @@ import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.services.dropbox.DropboxActivity import net.opendasharchive.openarchive.services.gdrive.GDriveActivity -import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveActivity +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveActivity import net.opendasharchive.openarchive.services.webdav.WebDavActivity import net.opendasharchive.openarchive.util.extensions.Position import net.opendasharchive.openarchive.util.extensions.getVersionName From bf021c77fd7a11baf73a45c383a6a91fc2ec8116 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Wed, 6 Mar 2024 10:21:19 -0800 Subject: [PATCH 32/63] fix(ia): add missing dependency for settings screen --- .../openarchive/features/internetarchive/Module.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index fc19e4ea..7811567f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -6,6 +6,7 @@ import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.I import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -20,5 +21,6 @@ val internetArchiveModule = module { factory { InternetArchiveMapper() } factory { InternetArchiveRepository(get(), get()) } factory { InternetArchiveLoginUseCase(get()) } + viewModel { args -> InternetArchiveViewModel(args.get()) } viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } From 2d1f0e06a76fe5bc9e14526fd817de38a66e5adf Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Wed, 6 Mar 2024 11:01:39 -0800 Subject: [PATCH 33/63] fix(ia): add local source for demo fix data model --- .../features/internetarchive/Module.kt | 6 ++-- .../domain/model/InternetArchive.kt | 4 +-- .../datasource/InternetArchiveLocalSource.kt | 18 ++++++++++ .../mapping/InternetArchiveMapper.kt | 4 +-- .../model/InternetArchiveLoginResponse.kt | 3 +- .../repository/InternetArchiveRepository.kt | 7 +++- .../presentation/InternetArchiveActivity.kt | 2 +- .../presentation/InternetArchiveScreen.kt | 22 +++++++----- .../presentation/InternetArchiveState.kt | 4 +-- .../presentation/InternetArchiveViewModel.kt | 34 +++++++++++++++++-- .../login/InternetArchiveLoginScreen.kt | 2 +- 11 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index 7811567f..6e6db19a 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -3,6 +3,7 @@ package net.opendasharchive.openarchive.features.internetarchive import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.InternetArchiveLoginUseCase +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveLocalSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository @@ -18,9 +19,10 @@ val internetArchiveModule = module { .create() } factory { InternetArchiveRemoteSource(get(), get()) } + single { InternetArchiveLocalSource() } factory { InternetArchiveMapper() } - factory { InternetArchiveRepository(get(), get()) } + factory { InternetArchiveRepository(get(), get(), get()) } factory { InternetArchiveLoginUseCase(get()) } - viewModel { args -> InternetArchiveViewModel(args.get()) } + viewModel { args -> InternetArchiveViewModel(get(), args.get()) } viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt index c6c2130e..e809cf88 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt @@ -1,8 +1,8 @@ package net.opendasharchive.openarchive.features.internetarchive.domain.model data class InternetArchive( - val username: String, + val userName: String, + val screenName: String, val email: String, - val expires: String, val auth: InternetArchiveAuth ) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt new file mode 100644 index 00000000..f1a0cde4 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt @@ -0,0 +1,18 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.update +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive + +class InternetArchiveLocalSource { + // TODO: just use a memory cache for demo, will need to store in DB + private val cache = MutableStateFlow(null) + + fun set(value: InternetArchive) = cache.update { value } + + fun get() = cache.value + + fun subscribe() = cache.filterNotNull() +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt index 95541e18..b045de8d 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt @@ -11,9 +11,9 @@ class InternetArchiveMapper { ) operator fun invoke(response: InternetArchiveLoginResponse.Values) = InternetArchive( - username = response.screenname ?: response.itemname ?: "", + userName = response.itemname ?: "", email = response.email ?: "", - expires = response.expires ?: "", + screenName = response.screenname ?: "", auth = response.s3?.let { invoke(it) } ?: InternetArchiveAuth("", "") ) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt index b81167eb..28734530 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt @@ -6,12 +6,11 @@ data class InternetArchiveLoginResponse( val version: Int, ) { data class Values( - val expires: String? = null, val s3: S3? = null, val screenname: String? = null, val email: String? = null, val itemname: String? = null, - val reason: String? = null + val reason: String? = null, ) data class S3( diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt index 41435d32..2ea52d8b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt @@ -1,9 +1,11 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveLocalSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest @@ -11,6 +13,7 @@ import net.opendasharchive.openarchive.features.internetarchive.infrastructure.m class InternetArchiveRepository( private val remoteSource: InternetArchiveRemoteSource, + private val localSource: InternetArchiveLocalSource, private val mapper: InternetArchiveMapper ) { suspend fun login(email: String, password: String): Result = @@ -24,9 +27,11 @@ class InternetArchiveRepository( when(response.version) { else -> mapper(response.values) } - } + }.onSuccess { localSource.set(it) } } + fun subscribe() = localSource.subscribe() + suspend fun testConnection(auth: InternetArchiveAuth): Result = withContext(Dispatchers.IO) { remoteSource.testConnection(auth).mapCatching { if (!it) throw UnauthenticatedException() } } 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 ff9137ec..68ebe874 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 @@ -37,7 +37,7 @@ class InternetArchiveActivity : AppCompatActivity() { startActivity(Intent(this, MainActivity::class.java)) measureNewBackend(Space.Type.INTERNET_ARCHIVE) } - IAResult.Cancelled -> Space.navigate(this) + IAResult.Deleted -> Space.navigate(this) else -> Unit } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt index 7bab2d5d..2c1d2900 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt @@ -46,6 +46,7 @@ fun InternetArchiveScreen(space: Space, onResult: (IAResult) -> Unit) { when (action) { is Action.Remove -> onResult(IAResult.Deleted) is Action.Cancel -> onResult(IAResult.Cancelled) + else -> Unit } } } @@ -56,7 +57,7 @@ fun InternetArchiveScreen(space: Space, onResult: (IAResult) -> Unit) { @Composable private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispatch) { - var isRemoving: Boolean by remember { mutableStateOf(false) } + var isRemoving by remember { mutableStateOf(false) } Box( modifier = Modifier @@ -70,31 +71,33 @@ private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispat Text( modifier = Modifier.padding(top = 12.dp), - text = stringResource(id = R.string.prompt_email), + text = "User Name", style = MaterialTheme.typography.caption ) Text( - text = state.email, + text = state.userName, fontSize = 18.sp ) + Text( modifier = Modifier.padding(top = 12.dp), - text = "Username", + text = "Screen Name", style = MaterialTheme.typography.caption ) + Text( - text = state.username, + text = state.screenName, fontSize = 18.sp ) Text( modifier = Modifier.padding(top = 12.dp), - text = "Expires", + text = "Email", style = MaterialTheme.typography.caption ) Text( - text = state.expires, + text = state.email, fontSize = 18.sp ) @@ -114,6 +117,7 @@ private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispat if (isRemoving) { RemoveInternetArchiveDialog(onDismiss = { isRemoving = false }) { + isRemoving = false dispatch(Action.Remove) } } @@ -147,8 +151,8 @@ private fun InternetArchiveScreenPreview() { InternetArchiveContent( state = InternetArchiveState( email = "abc@example.com", - username = "@abc_name", - expires = Date().toString() + userName = "@abc_name", + screenName = "ABC Name" ) ) {} } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt index dcffbe1d..7c4369fc 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt @@ -1,8 +1,8 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation data class InternetArchiveState( - val username: String = "", + val userName: String = "", + val screenName: String = "", val email: String = "", - val expires: String= "", ) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt index 3e3d906b..2fe13f54 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt @@ -1,13 +1,36 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import net.opendasharchive.openarchive.core.presentation.StatefulViewModel import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action -class InternetArchiveViewModel(private val space: Space) : - StatefulViewModel(InternetArchiveState()) { +class InternetArchiveViewModel( + private val repository: InternetArchiveRepository, + private val space: Space +) : StatefulViewModel(InternetArchiveState()) { - override fun reduce(state: InternetArchiveState, action: Action) = state + init { + viewModelScope.launch { + repository.subscribe().collect { + dispatch(Action.Loaded(it)) + } + } + } + + override fun reduce(state: InternetArchiveState, action: Action) = when(action) { + is Action.Loaded -> state.copy( + userName = action.value.userName, + email = action.value.email, + screenName = action.value.screenName + ) + else -> state + } override suspend fun effects(state: InternetArchiveState, action: Action) { when (action) { @@ -15,11 +38,16 @@ class InternetArchiveViewModel(private val space: Space) : space.delete() send(action) } + is Action.Cancel -> send(action) + else -> Unit } } sealed interface Action { + + data class Loaded(val value: InternetArchive) : Action + data object Remove : Action data object Cancel : Action 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 f9ffbea9..e5f04682 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 @@ -206,7 +206,7 @@ private fun InternetArchiveLoginContent( ) ) { if (state.isBusy) { - CircularProgressIndicator() + CircularProgressIndicator(color = colorResource(id = R.color.colorBackground)) } else { Text( text = stringResource(id = R.string.title_activity_login), From 8dd07adab3be68eecde52ca2286e99531ee5f661 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Wed, 6 Mar 2024 11:21:27 -0800 Subject: [PATCH 34/63] fix(ia): spinner color --- .../presentation/login/InternetArchiveLoginScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e5f04682..e8118254 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 @@ -206,7 +206,7 @@ private fun InternetArchiveLoginContent( ) ) { if (state.isBusy) { - CircularProgressIndicator(color = colorResource(id = R.color.colorBackground)) + CircularProgressIndicator(color = colorResource(id = R.color.colorPrimary)) } else { Text( text = stringResource(id = R.string.title_activity_login), From 1837ed8fce2d17e8da97c57de0500ba12353b7ac Mon Sep 17 00:00:00 2001 From: RJ Date: Thu, 7 Mar 2024 13:57:18 -0800 Subject: [PATCH 35/63] fix(main): ensure settings in adapter is always in line with button (#578) order --- .../opendasharchive/openarchive/features/main/ProjectAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt index 35e1d99c..8906074f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt @@ -19,7 +19,7 @@ class ProjectAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : } val settingsIndex: Int - get() = projects.size + get() = max(1, projects.size) fun updateData(projects: List) { this.projects = projects From 4a1fc671c4f6c92d5a3ae99677be2b71471ac7e5 Mon Sep 17 00:00:00 2001 From: RJ Date: Thu, 7 Mar 2024 13:57:45 -0800 Subject: [PATCH 36/63] fix(fastlane): add versioning plugin and ability to do a manual release build (#576) --- Gemfile | 10 ++ Gemfile.lock | 222 ++++++++++++++++++++++++++++++++++++++++++++ fastlane/Fastfile | 70 ++++++++++---- fastlane/Pluginfile | 1 + 4 files changed, 283 insertions(+), 20 deletions(-) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 fastlane/Pluginfile diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..b734015f --- /dev/null +++ b/Gemfile @@ -0,0 +1,10 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +source "https://rubygems.org" + +gem 'fastlane' + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..e9b03fb3 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,222 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.895.0) + aws-sdk-core (3.191.3) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.77.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.143.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.109.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.3.0) + fastlane (2.219.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-android_version_manager (0.4.1) + semantic (~> 1.6.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.7.1) + jwt (2.8.1) + base64 + mini_magick (4.12.0) + mini_mime (1.1.5) + multi_json (1.15.0) + multipart-post (2.4.0) + nanaimo (0.3.0) + naturally (2.2.1) + nkf (0.2.0) + optparse (0.4.0) + os (1.1.4) + plist (3.7.1) + public_suffix (5.0.4) + rake (13.1.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.6) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + semantic (1.6.1) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.5.0) + word_wrap (1.0.0) + xcodeproj (1.24.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + fastlane + fastlane-plugin-android_version_manager + +BUNDLED WITH + 2.5.6 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ab92687c..b3938036 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,30 +6,64 @@ platform :android do gradle(task: "test") end + desc "Create a release build for manual deployment" + lane :release do + gradle( + task: "assemble", + build_type: "release", + properties: { + "android.injected.signing.store.file" => ENV["FASTLANE_KEYSTORE_FILE"], + "android.injected.signing.store.password" => ENV["FASTLANE_KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => ENV["FASTLANE_KEY_ALIAS"], + "android.injected.signing.key.password" => ENV["FASTLANE_KEY_PASSWORD"], + } + ) + + send_progress_message("Copying APK to current folder") + copy_artifacts( + artifacts: [lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]], + target_path: ENV["PWD"] + ) + end + + desc "Increments the version code" + lane :increment_version_code do + android_increment_version_code( + app_project_dir: "./app" + ) + end + + desc "Increments the version name" + lane :increment_version_name do + android_increment_version_name( + app_project_dir: "./app" + ) + end + desc "Submit a new Internal Build" lane :internal do - send_progress_message("Build Started :rocket:") + send_progress_message("Build Started") gradle(task: "clean assembleRelease") - send_progress_message("Uploading To Internal track :rocket:") + send_progress_message("Uploading To Internal track") upload_to_play_store(track: "internal") end desc "Submit a new Alpha Build" lane :alpha do - send_progress_message("Build Started :rocket:") + send_progress_message("Build Started") gradle(task: "clean assembleRelease") - send_progress_message("Uploading To Alpha track :rocket:") + send_progress_message("Uploading To Alpha track") upload_to_play_store(track: "alpha") end desc "Submit a new Beta Build" lane :beta do - send_progress_message("Build Started :rocket:") + send_progress_message("Build Started 🚀") gradle(task: "clean assembleRelease") - send_progress_message("Uploading To Beta track :rocket:") + send_progress_message("Uploading To Beta track") upload_to_play_store(track: "beta") end @@ -53,18 +87,14 @@ def on_error(exception) end after_all do |lane| - file_name = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH].gsub(/\/.*\//,"") - send_message "Successfully deployed new App Update! :champagne:" - default_payloads = [ - :git_branch, - :last_git_commit_hash, - :last_git_commit_message - ] - payload = { - "Build Date" => Time.new.to_s, - "APK" => file_name - } - send_message file_name - send_message "#{default_payloads}" - send_message "#{payload}" + if lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH] + file_name = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH].gsub(/\/.*\//,"") + send_message "Successfully deployed new App Update!" + payload = { + "Build Date" => Time.new.to_s, + "APK" => file_name + } + send_message file_name + send_message "#{payload}" + end end diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile new file mode 100644 index 00000000..2866db63 --- /dev/null +++ b/fastlane/Pluginfile @@ -0,0 +1 @@ +gem 'fastlane-plugin-android_version_manager' From a5b228f305f9c1982e8e6b8b00dd4759157e2cf0 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Thu, 7 Mar 2024 14:51:05 -0800 Subject: [PATCH 37/63] fix(ia): implement compose theming fix bug in IA login use case that did not set credentials update compose material theme to v3 bump db version and add meta data column for provider extra info --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 2 +- app/src/main/assets/sugar_upgrades/35.sql | 1 + .../openarchive/core/di/FeaturesModule.kt | 3 +- .../infrastructure/client/ClientResult.kt | 3 +- .../core/presentation/theme/Colors.kt | 156 +++++++++++++++ .../core/presentation/theme/Dimensions.kt | 42 ++++ .../core/presentation/theme/Theme.kt | 29 +++ .../opendasharchive/openarchive/db/Space.kt | 1 + .../features/internetarchive/Module.kt | 8 +- .../domain/model/InternetArchive.kt | 20 +- .../domain/model/InternetArchiveAuth.kt | 6 - .../usecase/InternetArchiveLoginUseCase.kt | 21 +- .../datasource/InternetArchiveLocalSource.kt | 3 + .../datasource/InternetArchiveRemoteSource.kt | 4 +- .../mapping/InternetArchiveMapper.kt | 13 +- .../repository/InternetArchiveRepository.kt | 16 +- .../presentation/InternetArchiveActivity.kt | 13 +- .../presentation/InternetArchiveFragment.kt | 11 +- .../presentation/InternetArchiveScreen.kt | 165 ++-------------- .../components/InternetArchiveHeader.kt | 11 +- .../details/InternetArchiveDetailsScreen.kt | 185 ++++++++++++++++++ .../InternetArchiveDetailsState.kt} | 7 +- .../InternetArchiveDetailsViewModel.kt} | 32 +-- .../login/InternetArchiveLoginScreen.kt | 147 +++++++------- .../login/InternetArchiveLoginState.kt | 4 + .../login/InternetArchiveLoginViewModel.kt | 17 +- 27 files changed, 609 insertions(+), 315 deletions(-) create mode 100644 app/src/main/assets/sugar_upgrades/35.sql create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt delete mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt rename app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/{InternetArchiveState.kt => details/InternetArchiveDetailsState.kt} (57%) rename app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/{InternetArchiveViewModel.kt => details/InternetArchiveDetailsViewModel.kt} (59%) diff --git a/app/build.gradle b/app/build.gradle index b27a7b6b..09ec836f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -95,7 +95,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:2.9.0" implementation "androidx.compose.ui:ui:1.6.2" - implementation "androidx.compose.material:material:1.6.2" + implementation "androidx.compose.material3:material3:1.2.1" implementation 'androidx.compose.foundation:foundation:1.6.2' implementation "androidx.compose.ui:ui-tooling-preview:1.6.2" implementation "androidx.activity:activity-compose:1.8.2" @@ -114,7 +114,7 @@ dependencies { // 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 "com.github.bumptech.glide:glide:4.16.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 56ab137e..cdb18c46 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -228,7 +228,7 @@ + android:value="36" /> OkHttpClient.enqueueResult( + +suspend fun OkHttpClient.enqueueResult( request: Request, onResume: (Response) -> T ) = suspendCancellableCoroutine { continuation -> diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt new file mode 100644 index 00000000..428cfb0c --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt @@ -0,0 +1,156 @@ +package net.opendasharchive.openarchive.core.presentation.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +private val c23_nav_drawer_night = Color(0xff101010) +private val c23_darker_grey = Color(0xff212021) +private val c23_dark_grey = Color(0xff333333) +private val c23_medium_grey = Color(0xff696666) +private val c23_grey = Color(0xff9f9f9f) +private val c23_light_grey = Color(0xffe3e3e4) +private val c23_teal_100 = Color(0xff00ffeb) // h=175,3 s=100 v=100 --> +private val c23_teal_90 = Color(0xff00e7d5) // v=90.6 --> +private val c23_teal_80 = Color(0xff00cebe) // v=80.6 --> +private val c23_teal = Color(0xff00b4a6) // v=70.6 --> +private val c23_teal_60 = Color(0xff009b8f) // v=60.6 --> +private val c23_teal_50 = Color(0xff008177) // v=50.6 --> +private val c23_teal_40 = Color(0xff00685f) // v=40.6 --> +private val c23_teal_30 = Color(0xff004e48) // v=30.6 --> +private val c23_teal_20 = Color(0xff003530) // v=20.6 --> +private val c23_teal_10 = Color(0xff001b19) // v=10.6 --> +private val c23_powder_blue = Color(0xffaae6e1) + +@Immutable +data class ColorTheme( + val material: ColorScheme, + val primaryDark: Color = c23_teal_40, + val primaryBright: Color = c23_powder_blue, + + val colorBottomNavbar: Color = material.primary, + + val colorOnBottomNavbar: Color = material.onBackground, + + val colorAddButton: Color = material.background, + val colorOnAddButton: Color = material.onBackground, + val colorNavigationDrawerBackground: Color = material.background, + val colorOnboarding23GetStarted: Color = material.onBackground, + val colorSpaceSetupProgressOn: Color = Color.Black, + val colorSpaceSetupProgressOff: Color = c23_grey, + val colorBackgroundSpaceIcon: Color = c23_light_grey, + + + val colorPill: Color = Color(0xFFE3E3E4), + val colorMediaOverlayIcon: Color = Color.White, + val colorDanger: Color = material.error, + val colorDivider: Color = Color.LightGray, + val colorImageBackground: Color = Color.Black, + val colorFloatIconBackground: Color = Color.Transparent, + val colorSectionHeaderText: Color = Color.Gray, + val colorMediaTitleText: Color = Color.LightGray, + val colorWaveformIndicator: Color = Color(0xffaa0000), + val colorWaveform: Color = Color(0xFF999999) + +) + +private val LightColorScheme = ColorTheme( + material = lightColorScheme( + + primary = c23_teal, + onPrimary = Color.Black, + primaryContainer = c23_teal_90, + onPrimaryContainer = Color.Black, + + secondary = c23_teal, + onSecondary = Color.Black, + secondaryContainer = c23_teal_90, + onSecondaryContainer = Color.Black, + + tertiary = c23_powder_blue, + onTertiary = Color.Black, + tertiaryContainer = c23_powder_blue, + onTertiaryContainer = Color.Black, + + error = Color.Red, + onError = Color.White, + errorContainer = Color.Red, + onErrorContainer = Color.White, + + background = Color.Black, + onBackground = Color.White, + + surface = c23_light_grey, + onSurface = Color.Black, + surfaceVariant = c23_grey, + onSurfaceVariant = Color.Black, + + outline = Color.Black, + inverseOnSurface = Color.White, + inverseSurface = Color.Black, + inversePrimary = Color.Black, + surfaceTint = c23_teal + ), +) + +private val DarkColorScheme = ColorTheme( + material = darkColorScheme( + primary = c23_teal, + onPrimary = Color.White, + primaryContainer = c23_teal_20, + onPrimaryContainer = Color.White, + + secondary = c23_teal, + onSecondary = Color.White, + secondaryContainer = c23_teal_20, + onSecondaryContainer = Color.White, + + tertiary = c23_powder_blue, + onTertiary = Color.Black, + tertiaryContainer = c23_powder_blue, + onTertiaryContainer = Color.Black, + + error = Color.Red, + onError = Color.White, + errorContainer = Color.Red, + onErrorContainer = Color.White, + + background = Color.Black, + onBackground = Color.White, + + surface = c23_darker_grey, + onSurface = Color.White, + surfaceVariant = c23_dark_grey, + onSurfaceVariant = Color.White, + + outline = Color.White, + inverseOnSurface = Color.Black, + inverseSurface = Color.White, + inversePrimary = Color.White, + surfaceTint = c23_teal + ), +) + +fun getThemeColors(isDarkTheme: Boolean) = if (isDarkTheme) DarkColorScheme else LightColorScheme + +val LocalColors = staticCompositionLocalOf { LightColorScheme } + +@Composable +fun textFieldColors() = TextFieldDefaults.colors( + focusedIndicatorColor = ThemeColors.material.primary, + unfocusedIndicatorColor = ThemeColors.material.primary, + focusedLabelColor = ThemeColors.material.primary, + unfocusedLabelColor = ThemeColors.material.primary, + cursorColor = ThemeColors.material.primary, + focusedContainerColor = ThemeColors.material.surface, + unfocusedContainerColor = ThemeColors.material.surface, + disabledContainerColor = ThemeColors.material.surface, + unfocusedTextColor = ThemeColors.material.onSurface, + focusedTextColor = ThemeColors.material.onSurface, + disabledTextColor = ThemeColors.material.onSurface +) \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt new file mode 100644 index 00000000..8c920a80 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt @@ -0,0 +1,42 @@ +package net.opendasharchive.openarchive.core.presentation.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Immutable +data class Elevations(val card: Dp = 12.dp) + +@Immutable +data class Icons( + val small: Dp = 24.dp, + val medium: Dp = 48.dp, + val large: Dp = 72.dp +) + +@Immutable +data class Padding( + val small: Dp = 8.dp, + val medium: Dp = 16.dp, + val large: Dp = 32.dp, +) + +@Immutable +data class DimensionsTheme( + val touchable: Dp = 48.dp, + val padding: Padding = Padding(), + val elevations: Elevations = Elevations(), + val icons: Icons = Icons(), + val bubbleArrow: Dp = 24.dp, + val buttonCorner: Dp = 4.dp +) + +private val DimensionsLight = DimensionsTheme() + +private val DimensionsDark = DimensionsTheme(elevations = Elevations(card = 0.dp)) + +fun getThemeDimensions(isDarkTheme: Boolean) = + if (isDarkTheme) DimensionsDark else DimensionsLight + +val LocalDimensions = staticCompositionLocalOf { DimensionsLight } \ 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 new file mode 100644 index 00000000..60161a12 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt @@ -0,0 +1,29 @@ +package net.opendasharchive.openarchive.core.presentation.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider + +@Composable +fun Theme(content: @Composable () -> Unit) { + val isDarkTheme = isSystemInDarkTheme() + + val colors = getThemeColors(isDarkTheme) + + val dimensions = getThemeDimensions(isDarkTheme) + + CompositionLocalProvider( + LocalDimensions provides dimensions, + LocalColors provides colors, + ) { + MaterialTheme( + colorScheme = colors.material, + content = content + ) + } +} + + +val ThemeColors: ColorTheme @Composable get() = LocalColors.current +val ThemeDimensions: DimensionsTheme @Composable get() = LocalDimensions.current \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt b/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt index e8f9e5dc..56d24470 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt @@ -28,6 +28,7 @@ data class Space( var displayname: String = "", var password: String = "", var host: String = "", + var metaData: String = "", private var licenseUrl: String? = null, private var chunking: Boolean? = null ) : SugarRecord() { diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index 6e6db19a..b197c213 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -7,7 +7,7 @@ import net.opendasharchive.openarchive.features.internetarchive.infrastructure.d import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository -import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel +import net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsViewModel import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -22,7 +22,7 @@ val internetArchiveModule = module { single { InternetArchiveLocalSource() } factory { InternetArchiveMapper() } factory { InternetArchiveRepository(get(), get(), get()) } - factory { InternetArchiveLoginUseCase(get()) } - viewModel { args -> InternetArchiveViewModel(get(), args.get()) } - viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } + factory { args -> InternetArchiveLoginUseCase(get(), get(), args.get()) } + viewModel { args -> InternetArchiveDetailsViewModel(get(), args.get()) } + viewModel { args -> InternetArchiveLoginViewModel(args.get()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt index e809cf88..843cefd6 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt @@ -1,8 +1,18 @@ package net.opendasharchive.openarchive.features.internetarchive.domain.model data class InternetArchive( - val userName: String, - val screenName: String, - val email: String, - val auth: InternetArchiveAuth -) + val meta: MetaData, + val auth: Auth +) { + data class MetaData( + val userName: String, + val screenName: String, + val email: String, + ) + + + data class Auth( + val access: String, + val secret: String, + ) +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt deleted file mode 100644 index c49806df..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.opendasharchive.openarchive.features.internetarchive.domain.model - -data class InternetArchiveAuth( - val access: String, - val secret: String, -) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt index e2c9bc1c..84ea461a 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt @@ -1,15 +1,32 @@ package net.opendasharchive.openarchive.features.internetarchive.domain.usecase +import com.google.gson.Gson +import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository class InternetArchiveLoginUseCase( - private val repository: InternetArchiveRepository + private val repository: InternetArchiveRepository, + private val gson: Gson, + private val space: Space, ) { suspend operator fun invoke(email: String, password: String): Result = repository.login(email, password).mapCatching { response -> - repository.testConnection(response.auth).getOrThrow() + + response.auth.let { auth -> + repository.testConnection(auth).getOrThrow() + space.username = auth.access + space.password = auth.secret + } + + // TODO: use local data source for database + space.metaData = gson.toJson(response.meta) + space.save() + + Space.current = space + response } + } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt index f1a0cde4..01037892 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt @@ -8,6 +8,9 @@ import net.opendasharchive.openarchive.features.internetarchive.domain.model.Int class InternetArchiveLocalSource { // TODO: just use a memory cache for demo, will need to store in DB + // the database should be SQLCipher (https://www.zetetic.net/sqlcipher/) + // as we are storing access keys. Sugar record does not support sql cipher + // so planning a migration using local data sources. private val cache = MutableStateFlow(null) fun set(value: InternetArchive) = cache.update { value } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt index 47f0bf26..c8d1d74f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt @@ -3,7 +3,7 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure. import android.content.Context import com.google.gson.Gson import net.opendasharchive.openarchive.core.infrastructure.client.enqueueResult -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse import net.opendasharchive.openarchive.services.SaveClient @@ -34,7 +34,7 @@ class InternetArchiveRemoteSource( Result.success(data) } - suspend fun testConnection(auth: InternetArchiveAuth): Result = + suspend fun testConnection(auth: InternetArchive.Auth): Result = SaveClient.get(context).enqueueResult( Request.Builder() .url(ARCHIVE_API_ENDPOINT) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt index b045de8d..665403c7 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt @@ -1,19 +1,20 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse class InternetArchiveMapper { - private operator fun invoke(response: InternetArchiveLoginResponse.S3) = InternetArchiveAuth( + private operator fun invoke(response: InternetArchiveLoginResponse.S3) = InternetArchive.Auth( access = response.access, secret = response.secret ) operator fun invoke(response: InternetArchiveLoginResponse.Values) = InternetArchive( - userName = response.itemname ?: "", - email = response.email ?: "", - screenName = response.screenname ?: "", - auth = response.s3?.let { invoke(it) } ?: InternetArchiveAuth("", "") + meta = InternetArchive.MetaData( + userName = response.itemname ?: "", + email = response.email ?: "", + screenName = response.screenname ?: "" + ), + auth = response.s3?.let { invoke(it) } ?: InternetArchive.Auth("", "") ) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt index 2ea52d8b..cec01ebc 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt @@ -1,10 +1,8 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveLocalSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper @@ -24,15 +22,15 @@ class InternetArchiveRepository( if (response.success.not()) { throw IllegalArgumentException(response.values.reason) } - when(response.version) { - else -> mapper(response.values) + when (response.version) { + else -> mapper(response.values) } }.onSuccess { localSource.set(it) } } - fun subscribe() = localSource.subscribe() - - suspend fun testConnection(auth: InternetArchiveAuth): Result = withContext(Dispatchers.IO) { - remoteSource.testConnection(auth).mapCatching { if (!it) throw UnauthenticatedException() } - } + suspend fun testConnection(auth: InternetArchive.Auth): Result = + withContext(Dispatchers.IO) { + remoteSource.testConnection(auth) + .mapCatching { if (!it) throw UnauthenticatedException() } + } } 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 68ebe874..3f3cee65 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 @@ -9,24 +9,18 @@ import net.opendasharchive.openarchive.CleanInsightsManager 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.InternetArchiveLoginScreen import net.opendasharchive.openarchive.features.main.MainActivity @Deprecated("use jetpack compose") class InternetArchiveActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val (space, isNewSpace) = intent.extras.getSpace(Space.Type.INTERNET_ARCHIVE) setContent { - if (isNewSpace) { - InternetArchiveLoginScreen(space) { - finish(it) - } - } else { - InternetArchiveScreen(space) { - finish(it) - } + InternetArchiveScreen(space, isNewSpace) { + finish(it) } } } @@ -37,6 +31,7 @@ class InternetArchiveActivity : AppCompatActivity() { startActivity(Intent(this, MainActivity::class.java)) measureNewBackend(Space.Type.INTERNET_ARCHIVE) } + IAResult.Deleted -> Space.navigate(this) else -> Unit } 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 2c7a11ba..45313cae 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 @@ -13,7 +13,6 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.com 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.internetarchive.presentation.login.InternetArchiveLoginScreen @Deprecated("only used for backward compatibility") class InternetArchiveFragment : Fragment() { @@ -28,14 +27,8 @@ class InternetArchiveFragment : Fragment() { return ComposeView(requireContext()).apply { setContent { - if (isNewSpace) { - InternetArchiveLoginScreen(space) { result -> - finish(result) - } - } else { - InternetArchiveScreen(space) { result -> - finish(result) - } + InternetArchiveScreen(space, isNewSpace) { result -> + finish(result) } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt index 2c1d2900..36338e8d 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt @@ -1,164 +1,21 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.AlertDialog -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedButton -import androidx.compose.material.Text 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.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.core.state.Dispatch +import net.opendasharchive.openarchive.core.presentation.theme.Theme import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult -import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader -import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf -import java.util.Date +import net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsScreen +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen @Composable -fun InternetArchiveScreen(space: Space, onResult: (IAResult) -> Unit) { - val viewModel: InternetArchiveViewModel = koinViewModel { - parametersOf(space) - } - - val state by viewModel.state.collectAsState() - - LaunchedEffect(Unit) { - viewModel.effects.collect { action -> - when (action) { - is Action.Remove -> onResult(IAResult.Deleted) - is Action.Cancel -> onResult(IAResult.Cancelled) - else -> Unit - } +fun InternetArchiveScreen(space: Space, isNewSpace: Boolean, onFinish: (IAResult) -> Unit) = Theme { + if (isNewSpace) { + InternetArchiveLoginScreen(space) { + onFinish(it) } - } - - InternetArchiveContent(state, viewModel::dispatch) -} - -@Composable -private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispatch) { - - var isRemoving by remember { mutableStateOf(false) } - - Box( - modifier = Modifier - .fillMaxSize() - .padding(24.dp) - ) { - - Column { - - InternetArchiveHeader() - - Text( - modifier = Modifier.padding(top = 12.dp), - text = "User Name", - style = MaterialTheme.typography.caption - ) - Text( - text = state.userName, - fontSize = 18.sp - ) - - Text( - modifier = Modifier.padding(top = 12.dp), - text = "Screen Name", - style = MaterialTheme.typography.caption - ) - - Text( - text = state.screenName, - fontSize = 18.sp - ) - - Text( - modifier = Modifier.padding(top = 12.dp), - text = "Email", - style = MaterialTheme.typography.caption - ) - - Text( - text = state.email, - fontSize = 18.sp - ) - - Button( - modifier = Modifier - .padding(top = 12.dp) - .align(Alignment.CenterHorizontally), - onClick = { - isRemoving = true - }, - colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error) - ) { - Text(stringResource(id = R.string.menu_delete)) - } + } else { + InternetArchiveDetailsScreen(space) { + onFinish(it) } } - - if (isRemoving) { - RemoveInternetArchiveDialog(onDismiss = { isRemoving = false }) { - isRemoving = false - dispatch(Action.Remove) - } - } -} - -@Composable -private fun RemoveInternetArchiveDialog(onDismiss: () -> Unit, onRemove: () -> Unit) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(id = R.string.remove_from_app))}, - text = { Text(stringResource(id = R.string.are_you_sure_you_want_to_remove_this_server_from_the_app))}, - dismissButton = { - OutlinedButton( - onClick = onDismiss - ) { - Text(stringResource(id = R.string.action_cancel)) - } - }, confirmButton = { - Button( - onClick = onRemove, - colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error) - ) { - Text(stringResource(id = R.string.remove)) - } - }) -} - -@Composable -@Preview(showBackground = true) -private fun InternetArchiveScreenPreview() { - InternetArchiveContent( - state = InternetArchiveState( - email = "abc@example.com", - userName = "@abc_name", - screenName = "ABC Name" - ) - ) {} -} - -@Composable -@Preview(showBackground = true) -private fun RemoveInternetArchiveDialogPreview() { - RemoveInternetArchiveDialog(onDismiss = { }) {} -} +} \ No newline at end of file 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 2c6ddb67..26b9a510 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 @@ -1,5 +1,6 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.components +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -8,7 +9,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -23,6 +24,8 @@ 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.LocalColors +import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors @Composable fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 18.sp) { @@ -46,10 +49,12 @@ fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 1 Text( text = stringResource(id = R.string.internet_archive), fontSize = titleSize, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, + color = ThemeColors.material.onSurface ) Text( - text = stringResource(id = R.string.internet_archive_description) + text = stringResource(id = R.string.internet_archive_description), + color = ThemeColors.material.onSurfaceVariant ) } } 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 new file mode 100644 index 00000000..3723e2d3 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt @@ -0,0 +1,185 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.details + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +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.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +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.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.ThemeColors +import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions +import net.opendasharchive.openarchive.core.state.Dispatch +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 org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf + +@Composable +fun InternetArchiveDetailsScreen(space: Space, onResult: (IAResult) -> Unit) { + val viewModel: InternetArchiveDetailsViewModel = koinViewModel { + parametersOf(space) + } + + val state by viewModel.state.collectAsState() + + LaunchedEffect(Unit) { + viewModel.effects.collect { action -> + when (action) { + is Action.Remove -> onResult(IAResult.Deleted) + is Action.Cancel -> onResult(IAResult.Cancelled) + else -> Unit + } + } + } + + InternetArchiveDetailsContent(state, viewModel::dispatch) +} + +@Composable +private fun InternetArchiveDetailsContent( + state: InternetArchiveDetailsState, + dispatch: Dispatch +) { + + var isRemoving by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + ) { + + Column { + + InternetArchiveHeader() + + Spacer(Modifier.height(ThemeDimensions.padding.large)) + + Text( + text = "User Name", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onBackground + ) + Text( + text = state.userName, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + modifier = Modifier.padding(top = 16.dp), + text = "Screen Name", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = state.screenName, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + modifier = Modifier.padding(top = 16.dp), + text = "Email", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = state.email, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground + ) + } + + Button( + modifier = Modifier + .padding(top = 12.dp) + .align(Alignment.BottomCenter), + shape = RoundedCornerShape(ThemeDimensions.buttonCorner), + onClick = { + isRemoving = true + }, + colors = ButtonDefaults.buttonColors(containerColor = ThemeColors.material.error) + ) { + Text(stringResource(id = R.string.menu_delete)) + } + } + + if (isRemoving) { + RemoveInternetArchiveDialog(onDismiss = { isRemoving = false }) { + isRemoving = false + dispatch(Action.Remove) + } + } +} + +@Composable +private fun RemoveInternetArchiveDialog(onDismiss: () -> Unit, onRemove: () -> Unit) { + AlertDialog( + onDismissRequest = onDismiss, + containerColor = ThemeColors.material.surface, + titleContentColor = ThemeColors.material.onSurface, + textContentColor = ThemeColors.material.onSurfaceVariant, + title = { + Text(text = stringResource(id = R.string.remove_from_app)) + }, + text = { Text(stringResource(id = R.string.are_you_sure_you_want_to_remove_this_server_from_the_app)) }, + dismissButton = { + OutlinedButton( + onClick = onDismiss, + shape = RoundedCornerShape(ThemeDimensions.buttonCorner) + ) { + Text(stringResource(id = R.string.action_cancel)) + } + }, confirmButton = { + Button( + onClick = onRemove, + shape = RoundedCornerShape(ThemeDimensions.buttonCorner), + colors = ButtonDefaults.buttonColors(containerColor = ThemeColors.material.error) + ) { + Text(stringResource(id = R.string.remove)) + } + }) +} + +@Composable +@Preview(showBackground = true) +private fun InternetArchiveScreenPreview() { + InternetArchiveDetailsContent( + state = InternetArchiveDetailsState( + email = "abc@example.com", + userName = "@abc_name", + screenName = "ABC Name" + ) + ) {} +} + +@Composable +@Preview(showBackground = true) +private fun RemoveInternetArchiveDialogPreview() { + RemoveInternetArchiveDialog(onDismiss = { }) {} +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsState.kt similarity index 57% rename from app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt rename to app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsState.kt index 7c4369fc..d81558c9 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsState.kt @@ -1,6 +1,9 @@ -package net.opendasharchive.openarchive.features.internetarchive.presentation +package net.opendasharchive.openarchive.features.internetarchive.presentation.details -data class InternetArchiveState( +import androidx.compose.runtime.Immutable + +@Immutable +data class InternetArchiveDetailsState( val userName: String = "", val screenName: String = "", val email: String = "", diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsViewModel.kt similarity index 59% rename from app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt rename to app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsViewModel.kt index 2fe13f54..88ea71f6 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsViewModel.kt @@ -1,29 +1,24 @@ -package net.opendasharchive.openarchive.features.internetarchive.presentation +package net.opendasharchive.openarchive.features.internetarchive.presentation.details import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel -import kotlinx.coroutines.flow.collect +import com.google.gson.Gson import kotlinx.coroutines.launch import net.opendasharchive.openarchive.core.presentation.StatefulViewModel import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository -import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action +import net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsViewModel.Action -class InternetArchiveViewModel( - private val repository: InternetArchiveRepository, +class InternetArchiveDetailsViewModel( + private val gson: Gson, private val space: Space -) : StatefulViewModel(InternetArchiveState()) { +) : StatefulViewModel(InternetArchiveDetailsState()) { init { - viewModelScope.launch { - repository.subscribe().collect { - dispatch(Action.Loaded(it)) - } - } + dispatch(Action.Load(space)) } - override fun reduce(state: InternetArchiveState, action: Action) = when(action) { + override fun reduce(state: InternetArchiveDetailsState, action: Action) = when(action) { is Action.Loaded -> state.copy( userName = action.value.userName, email = action.value.email, @@ -32,13 +27,18 @@ class InternetArchiveViewModel( else -> state } - override suspend fun effects(state: InternetArchiveState, action: Action) { + override suspend fun effects(state: InternetArchiveDetailsState, action: Action) { when (action) { is Action.Remove -> { space.delete() send(action) } + is Action.Load -> { + val metaData = gson.fromJson(space.metaData, InternetArchive.MetaData::class.java) + dispatch(Action.Loaded(metaData)) + } + is Action.Cancel -> send(action) else -> Unit } @@ -46,7 +46,9 @@ class InternetArchiveViewModel( sealed interface Action { - data class Loaded(val value: InternetArchive) : Action + data class Load(val value: Space) : Action + + data class Loaded(val value: InternetArchive.MetaData) : Action data object Remove : Action 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 e8118254..c7a7563b 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 @@ -1,6 +1,7 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login import android.content.Intent +import android.content.res.Configuration import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -16,16 +17,18 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedButton -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +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.material3.TextField +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -43,6 +46,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors +import net.opendasharchive.openarchive.core.presentation.theme.textFieldColors import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult @@ -63,18 +68,16 @@ fun InternetArchiveLoginScreen(space: Space, onResult: (IAResult) -> Unit) { val state by viewModel.state.collectAsState() - val launcher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult(), - onResult = {} - ) + val launcher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult(), + onResult = {}) LaunchedEffect(Unit) { viewModel.effects.collect { action -> when (action) { is CreateLogin -> launcher.launch( Intent( - Intent.ACTION_VIEW, - Uri.parse(CreateLogin.URI) + Intent.ACTION_VIEW, Uri.parse(CreateLogin.URI) ) ) @@ -92,8 +95,7 @@ fun InternetArchiveLoginScreen(space: Space, onResult: (IAResult) -> Unit) { @Composable private fun InternetArchiveLoginContent( - state: InternetArchiveLoginState, - dispatch: Dispatch + state: InternetArchiveLoginState, dispatch: Dispatch ) { LaunchedEffect(state.isLoginError) { @@ -109,17 +111,16 @@ private fun InternetArchiveLoginContent( .padding(24.dp) ) { Column( - Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally + Modifier + .align(Alignment.Center) + .padding(bottom = 20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { InternetArchiveHeader( - modifier = Modifier.padding(bottom = 24.dp), - titleSize = 32.sp + modifier = Modifier.padding(bottom = 24.dp), titleSize = 32.sp ) - TextField( - value = state.email, + TextField(value = state.email, enabled = !state.isBusy, onValueChange = { dispatch(UpdateEmail(it)) }, label = { @@ -135,13 +136,7 @@ private fun InternetArchiveLoginContent( keyboardType = KeyboardType.Email ), isError = state.isEmailError, - colors = colorResource(id = R.color.colorPrimary).let { - TextFieldDefaults.textFieldColors( - focusedIndicatorColor = it, - focusedLabelColor = it, - cursorColor = it - ) - } + colors = textFieldColors() ) Spacer(Modifier.height(12.dp)) @@ -164,82 +159,80 @@ private fun InternetArchiveLoginContent( imeAction = ImeAction.Go ), isError = state.isPasswordError, - colors = colorResource(id = R.color.colorPrimary).let { - TextFieldDefaults.textFieldColors( - focusedIndicatorColor = it, - focusedLabelColor = it, - cursorColor = it - ) - } + colors = textFieldColors(), ) AnimatedVisibility( modifier = Modifier.padding(top = 20.dp), visible = state.isLoginError, - enter = fadeIn(), exit = fadeOut() + enter = fadeIn(), + exit = fadeOut() ) { Text( text = stringResource(id = R.string.error_incorrect_username_or_password), - color = MaterialTheme.colors.error + color = MaterialTheme.colorScheme.error ) } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceAround - ) { - OutlinedButton( - colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colors.onSurface), - onClick = { dispatch(Action.Cancel) } - ) { - Text(stringResource(id = R.string.action_cancel)) - } - Button( - enabled = !state.isBusy, - onClick = { dispatch(Login) }, - colors = ButtonDefaults.buttonColors( - backgroundColor = colorResource(id = R.color.colorPrimary), - contentColor = colorResource(id = R.color.colorBackground) - ) - ) { - if (state.isBusy) { - CircularProgressIndicator(color = colorResource(id = R.color.colorPrimary)) - } else { - Text( - text = stringResource(id = R.string.title_activity_login), - fontSize = 18.sp, - ) - } - } - } - Row( modifier = Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically ) { Text( - text = "No account?" + text = "No account?", color = ThemeColors.material.onSurface ) - TextButton( - colors = ButtonDefaults.textButtonColors(contentColor = colorResource(id = R.color.colorPrimary)), + TextButton(colors = ButtonDefaults.textButtonColors(contentColor = colorResource(id = R.color.colorPrimary)), onClick = { dispatch(CreateLogin) }) { Text(text = "Create Login", fontSize = 16.sp, fontWeight = FontWeight.Black) } } } + + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(top = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround + ) { + OutlinedButton(colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface), + shape = RoundedCornerShape(4.dp), + onClick = { dispatch(Action.Cancel) }) { + Text( + text = stringResource(id = R.string.action_cancel), + color = ThemeColors.material.onSurface + ) + } + Button( + enabled = !state.isBusy, + shape = RoundedCornerShape(4.dp), + onClick = { dispatch(Login) }, + colors = ButtonDefaults.buttonColors( + containerColor = colorResource(id = R.color.colorPrimary), + contentColor = colorResource(id = R.color.colorBackground) + ) + ) { + if (state.isBusy) { + CircularProgressIndicator(color = colorResource(id = R.color.colorPrimary)) + } else { + Text( + text = stringResource(id = R.string.title_activity_login), + fontSize = 18.sp, + ) + } + } + } + } } @Composable -@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun InternetArchiveLoginPreview() { InternetArchiveLoginContent( state = InternetArchiveLoginState( - email = "user@example.org", - password = "abc123" + email = "user@example.org", password = "abc123" ) ) {} } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt index 08db583b..db6a6853 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt @@ -1,5 +1,9 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable + +@Immutable data class InternetArchiveLoginState( val email: String = "", val password: String = "", diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt index 7e2e530c..e2e94139 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -13,11 +13,19 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.log import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword +import org.koin.compose.koinInject +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.core.parameter.parametersOf +import org.koin.java.KoinJavaComponent.inject class InternetArchiveLoginViewModel( - private val loginUseCase: InternetArchiveLoginUseCase, private val space: Space, -) : StatefulViewModel(InternetArchiveLoginState()) { +) : StatefulViewModel(InternetArchiveLoginState()), KoinComponent { + + private val loginUseCase: InternetArchiveLoginUseCase by inject { + parametersOf(space) + } override fun reduce( state: InternetArchiveLoginState, @@ -37,7 +45,6 @@ class InternetArchiveLoginViewModel( is Login -> loginUseCase(state.email, state.password) .onSuccess { ia -> - space.saveAndSetCurrent() send(LoginSuccess(ia)) } .onFailure { dispatch(LoginError(it)) } @@ -66,8 +73,4 @@ class InternetArchiveLoginViewModel( data class UpdatePassword(val value: String) : Action } - private fun Space.saveAndSetCurrent() { - save() - Space.current = this - } } From c8b85f12e40309d55d088d5029264cc0ab934041 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Mon, 11 Mar 2024 16:40:46 -0700 Subject: [PATCH 38/63] fix(ia): apply ui changes --- .../presentation/components/PrimaryButton.kt | 9 + .../core/presentation/theme/Colors.kt | 44 ++-- .../core/presentation/theme/Dimensions.kt | 12 +- .../features/internetarchive/Module.kt | 4 +- .../ValidateLoginCredentialsUseCase.kt | 18 ++ .../components/InternetArchiveHeader.kt | 15 +- .../details/InternetArchiveDetailsScreen.kt | 14 +- .../login/InternetArchiveLoginScreen.kt | 205 ++++++++++-------- .../login/InternetArchiveLoginState.kt | 8 +- .../login/InternetArchiveLoginViewModel.kt | 31 ++- app/src/main/res/values/strings.xml | 15 +- 11 files changed, 227 insertions(+), 148 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/presentation/components/PrimaryButton.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/ValidateLoginCredentialsUseCase.kt diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/components/PrimaryButton.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/components/PrimaryButton.kt new file mode 100644 index 00000000..c7e2ff25 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/components/PrimaryButton.kt @@ -0,0 +1,9 @@ +package net.opendasharchive.openarchive.core.presentation.components + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.Button +import androidx.compose.runtime.Composable + +@Composable +fun PrimaryButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) = + Button(onClick = onClick, content = content) diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt index 428cfb0c..e7bfd37a 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt @@ -14,6 +14,7 @@ private val c23_darker_grey = Color(0xff212021) private val c23_dark_grey = Color(0xff333333) private val c23_medium_grey = Color(0xff696666) private val c23_grey = Color(0xff9f9f9f) +private val c23_grey_50 = Color(0xff777979) private val c23_light_grey = Color(0xffe3e3e4) private val c23_teal_100 = Color(0xff00ffeb) // h=175,3 s=100 v=100 --> private val c23_teal_90 = Color(0xff00e7d5) // v=90.6 --> @@ -33,6 +34,9 @@ data class ColorTheme( val primaryDark: Color = c23_teal_40, val primaryBright: Color = c23_powder_blue, + val disabledContainer: Color = c23_teal_20, + val onDisabledContainer: Color = c23_light_grey, + val colorBottomNavbar: Color = material.primary, val colorOnBottomNavbar: Color = material.onBackground, @@ -64,7 +68,7 @@ private val LightColorScheme = ColorTheme( primary = c23_teal, onPrimary = Color.Black, - primaryContainer = c23_teal_90, + primaryContainer = c23_teal, onPrimaryContainer = Color.Black, secondary = c23_teal, @@ -78,9 +82,9 @@ private val LightColorScheme = ColorTheme( onTertiaryContainer = Color.Black, error = Color.Red, - onError = Color.White, + onError = Color.Black, errorContainer = Color.Red, - onErrorContainer = Color.White, + onErrorContainer = Color.Black, background = Color.Black, onBackground = Color.White, @@ -92,7 +96,7 @@ private val LightColorScheme = ColorTheme( outline = Color.Black, inverseOnSurface = Color.White, - inverseSurface = Color.Black, + inverseSurface = c23_dark_grey, inversePrimary = Color.Black, surfaceTint = c23_teal ), @@ -101,14 +105,14 @@ private val LightColorScheme = ColorTheme( private val DarkColorScheme = ColorTheme( material = darkColorScheme( primary = c23_teal, - onPrimary = Color.White, - primaryContainer = c23_teal_20, - onPrimaryContainer = Color.White, + onPrimary = Color.Black, + primaryContainer = c23_teal, + onPrimaryContainer = Color.Black, secondary = c23_teal, - onSecondary = Color.White, + onSecondary = Color.Black, secondaryContainer = c23_teal_20, - onSecondaryContainer = Color.White, + onSecondaryContainer = Color.Black, tertiary = c23_powder_blue, onTertiary = Color.Black, @@ -116,9 +120,9 @@ private val DarkColorScheme = ColorTheme( onTertiaryContainer = Color.Black, error = Color.Red, - onError = Color.White, + onError = Color.Black, errorContainer = Color.Red, - onErrorContainer = Color.White, + onErrorContainer = Color.Black, background = Color.Black, onBackground = Color.White, @@ -129,8 +133,8 @@ private val DarkColorScheme = ColorTheme( onSurfaceVariant = Color.White, outline = Color.White, + inverseSurface = c23_light_grey, inverseOnSurface = Color.Black, - inverseSurface = Color.White, inversePrimary = Color.White, surfaceTint = c23_teal ), @@ -143,14 +147,14 @@ val LocalColors = staticCompositionLocalOf { LightColorScheme } @Composable fun textFieldColors() = TextFieldDefaults.colors( focusedIndicatorColor = ThemeColors.material.primary, - unfocusedIndicatorColor = ThemeColors.material.primary, focusedLabelColor = ThemeColors.material.primary, - unfocusedLabelColor = ThemeColors.material.primary, - cursorColor = ThemeColors.material.primary, focusedContainerColor = ThemeColors.material.surface, - unfocusedContainerColor = ThemeColors.material.surface, - disabledContainerColor = ThemeColors.material.surface, - unfocusedTextColor = ThemeColors.material.onSurface, focusedTextColor = ThemeColors.material.onSurface, - disabledTextColor = ThemeColors.material.onSurface -) \ No newline at end of file + unfocusedIndicatorColor = ThemeColors.material.onSurfaceVariant, + unfocusedContainerColor = ThemeColors.material.surfaceVariant, + unfocusedTextColor = ThemeColors.material.onSurfaceVariant, + unfocusedLabelColor = ThemeColors.material.primary, + cursorColor = ThemeColors.material.primary, + disabledContainerColor = ThemeColors.disabledContainer, + disabledTextColor = ThemeColors.onDisabledContainer +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt index 8c920a80..a813a0b6 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt @@ -16,20 +16,22 @@ data class Icons( ) @Immutable -data class Padding( +data class Spacing( + val xsmall: Dp = 4.dp, val small: Dp = 8.dp, val medium: Dp = 16.dp, - val large: Dp = 32.dp, + val large: Dp = 24.dp, + val xlarge: Dp = 32.dp ) @Immutable data class DimensionsTheme( val touchable: Dp = 48.dp, - val padding: Padding = Padding(), + val spacing: Spacing = Spacing(), val elevations: Elevations = Elevations(), val icons: Icons = Icons(), val bubbleArrow: Dp = 24.dp, - val buttonCorner: Dp = 4.dp + val roundedCorner: Dp = 8.dp ) private val DimensionsLight = DimensionsTheme() @@ -39,4 +41,4 @@ private val DimensionsDark = DimensionsTheme(elevations = Elevations(card = 0.dp fun getThemeDimensions(isDarkTheme: Boolean) = if (isDarkTheme) DimensionsDark else DimensionsLight -val LocalDimensions = staticCompositionLocalOf { DimensionsLight } \ No newline at end of file +val LocalDimensions = staticCompositionLocalOf { DimensionsLight } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index b197c213..2fd1a89e 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -3,6 +3,7 @@ package net.opendasharchive.openarchive.features.internetarchive import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.InternetArchiveLoginUseCase +import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.ValidateLoginCredentialsUseCase import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveLocalSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper @@ -18,11 +19,12 @@ val internetArchiveModule = module { .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create() } + factory { ValidateLoginCredentialsUseCase() } factory { InternetArchiveRemoteSource(get(), get()) } single { InternetArchiveLocalSource() } factory { InternetArchiveMapper() } factory { InternetArchiveRepository(get(), get(), get()) } factory { args -> InternetArchiveLoginUseCase(get(), get(), args.get()) } viewModel { args -> InternetArchiveDetailsViewModel(get(), args.get()) } - viewModel { args -> InternetArchiveLoginViewModel(args.get()) } + viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/ValidateLoginCredentialsUseCase.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/ValidateLoginCredentialsUseCase.kt new file mode 100644 index 00000000..d8054ee8 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/ValidateLoginCredentialsUseCase.kt @@ -0,0 +1,18 @@ +package net.opendasharchive.openarchive.features.internetarchive.domain.usecase + +class ValidateLoginCredentialsUseCase { + + operator fun invoke(identifier: String, factor: String): Boolean { + return if (identifier.contains('@')) { + validateEmail(identifier) + } else { + validateUsername(identifier) + } && validatePassword(factor) + } + + private fun validateEmail(identifier: String) = identifier.isNotBlank() + + private fun validateUsername(identifier: String) = identifier.isNotBlank() + + private fun validatePassword(factor: String) = factor.isNotBlank() +} 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 26b9a510..221fabec 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 @@ -1,13 +1,14 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.components -import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background +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.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -24,20 +25,20 @@ 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.LocalColors import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors +import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions @Composable fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 18.sp) { - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { Box(modifier = Modifier - .size(48.dp) + .size(ThemeDimensions.touchable) .background( - color = colorResource(id = R.color.colorBackgroundSpaceIcon), + color = ThemeColors.material.surface, shape = CircleShape ).clip(CircleShape)) { Image( - modifier = Modifier.matchParentSize().padding(12.dp), + modifier = Modifier.matchParentSize().padding(ThemeDimensions.spacing.small), painter = painterResource(id = R.drawable.ic_internet_archive), contentDescription = stringResource( id = R.string.internet_archive @@ -45,7 +46,7 @@ fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 1 colorFilter = tint(colorResource(id = R.color.colorPrimary)) ) } - Column(modifier = Modifier.padding(start = 8.dp)) { + Column(modifier = Modifier.padding(start = ThemeDimensions.spacing.small)) { Text( text = stringResource(id = R.string.internet_archive), fontSize = titleSize, 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 3723e2d3..10e0dd4c 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 @@ -75,10 +75,10 @@ private fun InternetArchiveDetailsContent( InternetArchiveHeader() - Spacer(Modifier.height(ThemeDimensions.padding.large)) + Spacer(Modifier.height(ThemeDimensions.spacing.large)) Text( - text = "User Name", + text = stringResource(id = R.string.label_username), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onBackground ) @@ -90,7 +90,7 @@ private fun InternetArchiveDetailsContent( Text( modifier = Modifier.padding(top = 16.dp), - text = "Screen Name", + text = stringResource(id = R.string.label_screen_name), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onBackground ) @@ -103,7 +103,7 @@ private fun InternetArchiveDetailsContent( Text( modifier = Modifier.padding(top = 16.dp), - text = "Email", + text = stringResource(id = R.string.label_email), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onBackground ) @@ -119,7 +119,7 @@ private fun InternetArchiveDetailsContent( modifier = Modifier .padding(top = 12.dp) .align(Alignment.BottomCenter), - shape = RoundedCornerShape(ThemeDimensions.buttonCorner), + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { isRemoving = true }, @@ -151,14 +151,14 @@ private fun RemoveInternetArchiveDialog(onDismiss: () -> Unit, onRemove: () -> U dismissButton = { OutlinedButton( onClick = onDismiss, - shape = RoundedCornerShape(ThemeDimensions.buttonCorner) + shape = RoundedCornerShape(ThemeDimensions.roundedCorner) ) { Text(stringResource(id = R.string.action_cancel)) } }, confirmButton = { Button( onClick = onRemove, - shape = RoundedCornerShape(ThemeDimensions.buttonCorner), + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), colors = ButtonDefaults.buttonColors(containerColor = ThemeColors.material.error) ) { Text(stringResource(id = R.string.remove)) 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 c7a7563b..2e81d3e6 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 @@ -9,7 +9,6 @@ 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 @@ -22,13 +21,10 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -37,16 +33,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource 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.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors +import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions import net.opendasharchive.openarchive.core.presentation.theme.textFieldColors import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space @@ -55,8 +49,8 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.com import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateUsername import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -101,129 +95,156 @@ private fun InternetArchiveLoginContent( LaunchedEffect(state.isLoginError) { while (state.isLoginError) { delay(3000) - dispatch(Action.ErrorFade) + dispatch(Action.ErrorClear) } } - Box( + Column( modifier = Modifier .fillMaxSize() - .padding(24.dp) + .padding(ThemeDimensions.spacing.medium), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - Modifier - .align(Alignment.Center) - .padding(bottom = 20.dp), horizontalAlignment = Alignment.CenterHorizontally - ) { - InternetArchiveHeader( - modifier = Modifier.padding(bottom = 24.dp), titleSize = 32.sp - ) + InternetArchiveHeader( + modifier = Modifier.padding(bottom = ThemeDimensions.spacing.large) + ) - TextField(value = state.email, - enabled = !state.isBusy, - onValueChange = { dispatch(UpdateEmail(it)) }, - label = { - Text( - text = stringResource(id = R.string.prompt_email), - color = colorResource(id = R.color.colorPrimary) - ) - }, - singleLine = true, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Next, - autoCorrect = false, - keyboardType = KeyboardType.Email - ), - isError = state.isEmailError, - colors = textFieldColors() - ) + val colors = textFieldColors() - Spacer(Modifier.height(12.dp)) + OutlinedTextField( + value = state.username, + enabled = !state.isBusy, + onValueChange = { dispatch(UpdateUsername(it)) }, + label = { + Text( + text = stringResource(id = R.string.label_username), + color = ThemeColors.material.onBackground + ) + }, + placeholder = { + Text( + text = stringResource(id = R.string.placeholder_email_or_username), + color = ThemeColors.material.onSurfaceVariant + ) + }, + singleLine = true, + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + autoCorrect = false, + keyboardType = KeyboardType.Email + ), + isError = state.isUsernameError, + colors = colors + ) - TextField( - value = state.password, - enabled = !state.isBusy, - onValueChange = { dispatch(UpdatePassword(it)) }, - label = { - Text( - stringResource(id = R.string.prompt_password), - color = colorResource(id = R.color.colorPrimary) - ) - }, - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - autoCorrect = false, - imeAction = ImeAction.Go - ), - isError = state.isPasswordError, - colors = textFieldColors(), - ) + Spacer(Modifier.height(ThemeDimensions.spacing.large)) - AnimatedVisibility( - modifier = Modifier.padding(top = 20.dp), - visible = state.isLoginError, - enter = fadeIn(), - exit = fadeOut() - ) { + OutlinedTextField( + value = state.password, + enabled = !state.isBusy, + onValueChange = { dispatch(UpdatePassword(it)) }, + label = { Text( - text = stringResource(id = R.string.error_incorrect_username_or_password), - color = MaterialTheme.colorScheme.error + stringResource(id = R.string.label_password), + color = ThemeColors.material.onBackground + ) + }, + placeholder = { + Text( + stringResource(id = R.string.placeholder_password), + color = ThemeColors.material.onSurfaceVariant ) - } - Row( - modifier = Modifier.padding(top = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { + }, + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + autoCorrect = false, + imeAction = ImeAction.Go + ), + isError = state.isPasswordError, + colors = colors, + ) + + AnimatedVisibility( + visible = state.isLoginError, + enter = fadeIn(), + exit = fadeOut() + ) { + Text( + text = stringResource(id = R.string.error_incorrect_username_or_password), + color = MaterialTheme.colorScheme.error + ) + } + Row( + modifier = Modifier + .padding(top = ThemeDimensions.spacing.medium) + .weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.prompt_no_account), + color = ThemeColors.material.onSurface + ) + TextButton( + colors = ButtonDefaults.textButtonColors( + contentColor = colorResource(id = R.color.colorPrimary) + ), + onClick = { dispatch(CreateLogin) }) { Text( - text = "No account?", color = ThemeColors.material.onSurface + text = stringResource(id = R.string.label_create_login), + style = MaterialTheme.typography.bodyLarge ) - TextButton(colors = ButtonDefaults.textButtonColors(contentColor = colorResource(id = R.color.colorPrimary)), - onClick = { dispatch(CreateLogin) }) { - Text(text = "Create Login", fontSize = 16.sp, fontWeight = FontWeight.Black) - } } } Row( modifier = Modifier .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(top = 20.dp), + .padding(top = ThemeDimensions.spacing.medium), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceAround + horizontalArrangement = Arrangement.SpaceEvenly ) { - OutlinedButton(colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface), - shape = RoundedCornerShape(4.dp), + TextButton( + modifier = Modifier + .weight(1f) + .padding(ThemeDimensions.spacing.small), + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.primary + ), + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { dispatch(Action.Cancel) }) { Text( - text = stringResource(id = R.string.action_cancel), - color = ThemeColors.material.onSurface + text = stringResource(id = R.string.action_cancel) ) } Button( - enabled = !state.isBusy, - shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .padding(ThemeDimensions.spacing.small), + enabled = !state.isBusy && state.isValid, + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { dispatch(Login) }, colors = ButtonDefaults.buttonColors( - containerColor = colorResource(id = R.color.colorPrimary), - contentColor = colorResource(id = R.color.colorBackground) + containerColor = ThemeColors.material.primaryContainer, + contentColor = ThemeColors.material.onPrimaryContainer, + disabledContainerColor = ThemeColors.disabledContainer, + disabledContentColor = ThemeColors.onDisabledContainer ) ) { if (state.isBusy) { CircularProgressIndicator(color = colorResource(id = R.color.colorPrimary)) } else { Text( - text = stringResource(id = R.string.title_activity_login), - fontSize = 18.sp, + text = stringResource(id = R.string.label_login), + style = MaterialTheme.typography.bodyLarge, ) } } } - } } @@ -232,7 +253,7 @@ private fun InternetArchiveLoginContent( private fun InternetArchiveLoginPreview() { InternetArchiveLoginContent( state = InternetArchiveLoginState( - email = "user@example.org", password = "abc123" + username = "user@example.org", password = "abc123" ) ) {} } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt index db6a6853..28ab03c1 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt @@ -1,14 +1,14 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable @Immutable data class InternetArchiveLoginState( - val email: String = "", + val username: String = "", val password: String = "", - val isEmailError: Boolean = false, + val isUsernameError: Boolean = false, val isPasswordError: Boolean = false, val isLoginError: Boolean = false, - val isBusy: Boolean = false + val isBusy: Boolean = false, + val isValid: Boolean = false, ) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt index e2e94139..9d212c58 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -4,24 +4,25 @@ import net.opendasharchive.openarchive.core.presentation.StatefulViewModel import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.InternetArchiveLoginUseCase +import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.ValidateLoginCredentialsUseCase import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Cancel import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.ErrorFade +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.ErrorClear import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginError import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword -import org.koin.compose.koinInject +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateUsername import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koin.core.parameter.parametersOf -import org.koin.java.KoinJavaComponent.inject class InternetArchiveLoginViewModel( + private val validateLoginCredentials: ValidateLoginCredentialsUseCase, private val space: Space, -) : StatefulViewModel(InternetArchiveLoginState()), KoinComponent { +) : StatefulViewModel(InternetArchiveLoginState()), + KoinComponent { private val loginUseCase: InternetArchiveLoginUseCase by inject { parametersOf(space) @@ -31,19 +32,27 @@ class InternetArchiveLoginViewModel( state: InternetArchiveLoginState, action: Action ): InternetArchiveLoginState = when (action) { - is UpdateEmail -> state.copy(email = action.value) - is UpdatePassword -> state.copy(password = action.value) + is UpdateUsername -> state.copy( + username = action.value, + isValid = validateLoginCredentials(action.value, state.password) + ) + + is UpdatePassword -> state.copy( + password = action.value, + isValid = validateLoginCredentials(state.username, action.value) + ) + is Login -> state.copy(isBusy = true) is LoginError -> state.copy(isLoginError = true, isBusy = false) is LoginSuccess, is Cancel -> state.copy(isBusy = false) - is ErrorFade -> state.copy(isLoginError = false) + is ErrorClear -> state.copy(isLoginError = false) else -> state } override suspend fun effects(state: InternetArchiveLoginState, action: Action) { when (action) { is Login -> - loginUseCase(state.email, state.password) + loginUseCase(state.username, state.password) .onSuccess { ia -> send(LoginSuccess(ia)) } @@ -63,13 +72,13 @@ class InternetArchiveLoginViewModel( data class LoginError(val value: Throwable) : Action - data object ErrorFade : Action + data object ErrorClear : Action data object CreateLogin : Action { const val URI = "https://archive.org/account/signup" } - data class UpdateEmail(val value: String) : Action + data class UpdateUsername(val value: String) : Action data class UpdatePassword(val value: String) : Action } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a46ca1d7..60dca8c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,7 +20,7 @@ Internet Archive - Upload and preserve media in a digital library + Upload your media to a public server. Google Drive Google Drive™ Upload to Google Drive @@ -89,6 +89,12 @@ Username Password + + @string/prompt_email + Enter a email or username + @string/prompt_password + Enter a password + Sign in Incorrect username or password This field is required @@ -286,4 +292,11 @@ %1$s cannot work properly if you don\'t allow it to write to your %2$s. Please try the authorization again and make sure to grant all the access permissions listed. new user + + Email + Screen Name + Login + Create Login + No account? + From 212ab9d390f6eaad009e56055c2060515a1b3a64 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Mon, 11 Mar 2024 22:20:39 -0700 Subject: [PATCH 39/63] fix(ia): fix uploads and simplify theme --- .../core/presentation/theme/Colors.kt | 67 +++++------------ .../core/presentation/theme/Theme.kt | 10 ++- .../components/InternetArchiveHeader.kt | 28 ++++--- .../login/InternetArchiveLoginScreen.kt | 74 ++++++------------- .../openarchive/features/main/MainActivity.kt | 60 ++++++++------- .../features/main/MainMediaFragment.kt | 8 +- .../openarchive/services/SaveClient.kt | 6 +- .../services/internetarchive/IaConduit.kt | 16 +--- .../internetarchive/RequestBodyUtil.kt | 28 +++++-- .../openarchive/upload/BroadcastManager.kt | 4 +- .../layout/fragment_space_setup_success.xml | 6 +- app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 4 + 13 files changed, 140 insertions(+), 172 deletions(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt index e7bfd37a..e4e8db82 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt @@ -1,10 +1,8 @@ package net.opendasharchive.openarchive.core.presentation.theme import androidx.compose.material3.ColorScheme -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color @@ -36,31 +34,6 @@ data class ColorTheme( val disabledContainer: Color = c23_teal_20, val onDisabledContainer: Color = c23_light_grey, - - val colorBottomNavbar: Color = material.primary, - - val colorOnBottomNavbar: Color = material.onBackground, - - val colorAddButton: Color = material.background, - val colorOnAddButton: Color = material.onBackground, - val colorNavigationDrawerBackground: Color = material.background, - val colorOnboarding23GetStarted: Color = material.onBackground, - val colorSpaceSetupProgressOn: Color = Color.Black, - val colorSpaceSetupProgressOff: Color = c23_grey, - val colorBackgroundSpaceIcon: Color = c23_light_grey, - - - val colorPill: Color = Color(0xFFE3E3E4), - val colorMediaOverlayIcon: Color = Color.White, - val colorDanger: Color = material.error, - val colorDivider: Color = Color.LightGray, - val colorImageBackground: Color = Color.Black, - val colorFloatIconBackground: Color = Color.Transparent, - val colorSectionHeaderText: Color = Color.Gray, - val colorMediaTitleText: Color = Color.LightGray, - val colorWaveformIndicator: Color = Color(0xffaa0000), - val colorWaveform: Color = Color(0xFF999999) - ) private val LightColorScheme = ColorTheme( @@ -86,19 +59,24 @@ private val LightColorScheme = ColorTheme( errorContainer = Color.Red, onErrorContainer = Color.Black, - background = Color.Black, - onBackground = Color.White, + background = Color.White, + onBackground = Color.Black, surface = c23_light_grey, onSurface = Color.Black, surfaceVariant = c23_grey, - onSurfaceVariant = Color.Black, + onSurfaceVariant = c23_darker_grey, outline = Color.Black, inverseOnSurface = Color.White, inverseSurface = c23_dark_grey, inversePrimary = Color.Black, - surfaceTint = c23_teal + surfaceTint = c23_teal, + outlineVariant = c23_darker_grey, + scrim = c23_light_grey, + surfaceBright = c23_light_grey, + surfaceContainer = Color.White, + surfaceDim = c23_light_grey ), ) @@ -107,12 +85,12 @@ private val DarkColorScheme = ColorTheme( primary = c23_teal, onPrimary = Color.Black, primaryContainer = c23_teal, - onPrimaryContainer = Color.Black, + onPrimaryContainer = Color.White, secondary = c23_teal, onSecondary = Color.Black, secondaryContainer = c23_teal_20, - onSecondaryContainer = Color.Black, + onSecondaryContainer = Color.White, tertiary = c23_powder_blue, onTertiary = Color.Black, @@ -130,13 +108,18 @@ private val DarkColorScheme = ColorTheme( surface = c23_darker_grey, onSurface = Color.White, surfaceVariant = c23_dark_grey, - onSurfaceVariant = Color.White, + onSurfaceVariant = c23_light_grey, outline = Color.White, inverseSurface = c23_light_grey, inverseOnSurface = Color.Black, inversePrimary = Color.White, - surfaceTint = c23_teal + surfaceTint = c23_teal, + outlineVariant = c23_light_grey, + scrim = c23_light_grey, + surfaceBright = c23_grey, + surfaceContainer = c23_medium_grey, + surfaceDim = c23_dark_grey ), ) @@ -144,17 +127,3 @@ fun getThemeColors(isDarkTheme: Boolean) = if (isDarkTheme) DarkColorScheme else val LocalColors = staticCompositionLocalOf { LightColorScheme } -@Composable -fun textFieldColors() = TextFieldDefaults.colors( - focusedIndicatorColor = ThemeColors.material.primary, - focusedLabelColor = ThemeColors.material.primary, - focusedContainerColor = ThemeColors.material.surface, - focusedTextColor = ThemeColors.material.onSurface, - unfocusedIndicatorColor = ThemeColors.material.onSurfaceVariant, - unfocusedContainerColor = ThemeColors.material.surfaceVariant, - unfocusedTextColor = ThemeColors.material.onSurfaceVariant, - unfocusedLabelColor = ThemeColors.material.primary, - cursorColor = ThemeColors.material.primary, - disabledContainerColor = ThemeColors.disabledContainer, - disabledTextColor = ThemeColors.onDisabledContainer -) 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 60161a12..e0bfbea0 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 @@ -4,10 +4,14 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState @Composable -fun Theme(content: @Composable () -> Unit) { - val isDarkTheme = isSystemInDarkTheme() +fun Theme( + content: @Composable () -> Unit +) { + val isDarkTheme by rememberUpdatedState(newValue = isSystemInDarkTheme()) val colors = getThemeColors(isDarkTheme) @@ -26,4 +30,4 @@ fun Theme(content: @Composable () -> Unit) { val ThemeColors: ColorTheme @Composable get() = LocalColors.current -val ThemeDimensions: DimensionsTheme @Composable get() = LocalDimensions.current \ No newline at end of file +val ThemeDimensions: DimensionsTheme @Composable get() = LocalDimensions.current 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 221fabec..2f6d1a07 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 @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,15 +29,24 @@ import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions @Composable fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 18.sp) { - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { - Box(modifier = Modifier - .size(ThemeDimensions.touchable) - .background( - color = ThemeColors.material.surface, - shape = CircleShape - ).clip(CircleShape)) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .size(ThemeDimensions.touchable) + .background( + color = ThemeColors.material.surface, + shape = CircleShape + ) + .clip(CircleShape) + ) { Image( - modifier = Modifier.matchParentSize().padding(ThemeDimensions.spacing.small), + modifier = Modifier + .matchParentSize() + .padding(11.dp), painter = painterResource(id = R.drawable.ic_internet_archive), contentDescription = stringResource( id = R.string.internet_archive @@ -46,7 +54,7 @@ fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 1 colorFilter = tint(colorResource(id = R.color.colorPrimary)) ) } - Column(modifier = Modifier.padding(start = ThemeDimensions.spacing.small)) { + Column(modifier = Modifier.padding(start = ThemeDimensions.spacing.medium)) { Text( text = stringResource(id = R.string.internet_archive), fontSize = titleSize, 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 2e81d3e6..28fd81a4 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 @@ -1,7 +1,6 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login import android.content.Intent -import android.content.res.Configuration import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -15,11 +14,11 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -31,8 +30,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource 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.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -41,7 +40,6 @@ import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions -import net.opendasharchive.openarchive.core.presentation.theme.textFieldColors import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult @@ -110,23 +108,15 @@ private fun InternetArchiveLoginContent( modifier = Modifier.padding(bottom = ThemeDimensions.spacing.large) ) - val colors = textFieldColors() - OutlinedTextField( value = state.username, enabled = !state.isBusy, onValueChange = { dispatch(UpdateUsername(it)) }, label = { - Text( - text = stringResource(id = R.string.label_username), - color = ThemeColors.material.onBackground - ) + Text(stringResource(R.string.label_username)) }, placeholder = { - Text( - text = stringResource(id = R.string.placeholder_email_or_username), - color = ThemeColors.material.onSurfaceVariant - ) + Text(stringResource(R.string.placeholder_email_or_username)) }, singleLine = true, shape = RoundedCornerShape(ThemeDimensions.roundedCorner), @@ -136,7 +126,6 @@ private fun InternetArchiveLoginContent( keyboardType = KeyboardType.Email ), isError = state.isUsernameError, - colors = colors ) Spacer(Modifier.height(ThemeDimensions.spacing.large)) @@ -146,19 +135,13 @@ private fun InternetArchiveLoginContent( enabled = !state.isBusy, onValueChange = { dispatch(UpdatePassword(it)) }, label = { - Text( - stringResource(id = R.string.label_password), - color = ThemeColors.material.onBackground - ) + Text(stringResource(R.string.label_password)) }, placeholder = { - Text( - stringResource(id = R.string.placeholder_password), - color = ThemeColors.material.onSurfaceVariant - ) - + Text(stringResource(R.string.placeholder_password)) }, singleLine = true, + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password, @@ -166,7 +149,6 @@ private fun InternetArchiveLoginContent( imeAction = ImeAction.Go ), isError = state.isPasswordError, - colors = colors, ) AnimatedVisibility( @@ -175,27 +157,26 @@ private fun InternetArchiveLoginContent( exit = fadeOut() ) { Text( - text = stringResource(id = R.string.error_incorrect_username_or_password), + text = stringResource(R.string.error_incorrect_username_or_password), color = MaterialTheme.colorScheme.error ) } Row( modifier = Modifier - .padding(top = ThemeDimensions.spacing.medium) + .padding(top = ThemeDimensions.spacing.small) .weight(1f), verticalAlignment = Alignment.CenterVertically ) { Text( - text = stringResource(id = R.string.prompt_no_account), - color = ThemeColors.material.onSurface + text = stringResource(R.string.prompt_no_account), + color = ThemeColors.material.onBackground ) TextButton( - colors = ButtonDefaults.textButtonColors( - contentColor = colorResource(id = R.color.colorPrimary) - ), + modifier = Modifier.heightIn(ThemeDimensions.touchable), onClick = { dispatch(CreateLogin) }) { Text( - text = stringResource(id = R.string.label_create_login), + text = stringResource(R.string.label_create_login), + fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyLarge ) } @@ -211,37 +192,24 @@ private fun InternetArchiveLoginContent( TextButton( modifier = Modifier .weight(1f) + .heightIn(ThemeDimensions.touchable) .padding(ThemeDimensions.spacing.small), - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.primary - ), shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { dispatch(Action.Cancel) }) { - Text( - text = stringResource(id = R.string.action_cancel) - ) + Text(stringResource(R.string.action_cancel)) } Button( modifier = Modifier - .weight(1f) - .padding(ThemeDimensions.spacing.small), + .heightIn(ThemeDimensions.touchable) + .weight(1f), enabled = !state.isBusy && state.isValid, shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { dispatch(Login) }, - colors = ButtonDefaults.buttonColors( - containerColor = ThemeColors.material.primaryContainer, - contentColor = ThemeColors.material.onPrimaryContainer, - disabledContainerColor = ThemeColors.disabledContainer, - disabledContentColor = ThemeColors.onDisabledContainer - ) ) { if (state.isBusy) { - CircularProgressIndicator(color = colorResource(id = R.color.colorPrimary)) + CircularProgressIndicator(color = ThemeColors.material.primary) } else { - Text( - text = stringResource(id = R.string.label_login), - style = MaterialTheme.typography.bodyLarge, - ) + Text(stringResource(R.string.label_login)) } } } @@ -249,7 +217,7 @@ private fun InternetArchiveLoginContent( } @Composable -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true) private fun InternetArchiveLoginPreview() { InternetArchiveLoginContent( state = InternetArchiveLoginState( 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 34e4aefc..e3f27aed 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 @@ -297,15 +297,17 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener private fun refreshSpace() { val currentSpace = Space.current - if (currentSpace != null) { - mBinding.space.setDrawable( - currentSpace.getAvatar(this) - ?.scaled(32, this), Position.Start, tint = false - ) - mBinding.space.text = currentSpace.friendlyName - } else { - mBinding.space.setDrawable(R.drawable.avatar_default, Position.Start, tint = false) - mBinding.space.text = getString(R.string.app_name) + MainScope().launch { + if (currentSpace != null) { + mBinding.space.setDrawable( + currentSpace.getAvatar(this@MainActivity) + ?.scaled(32, this@MainActivity), Position.Start, tint = false + ) + mBinding.space.text = 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()) @@ -332,17 +334,19 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener private fun refreshCurrentProject() { val project = getSelectedProject() - if (project != null) { - mPagerAdapter.notifyProjectChanged(project) + MainScope().launch { + if (project != null) { + mPagerAdapter.notifyProjectChanged(project) - project.space?.setAvatar(mBinding.currentFolderIcon) - mBinding.currentFolderIcon.show() + project.space?.setAvatar(mBinding.currentFolderIcon) + mBinding.currentFolderIcon.show() - mBinding.currentFolderName.text = project.description - mBinding.currentFolderName.show() - } else { - mBinding.currentFolderIcon.cloak() - mBinding.currentFolderName.cloak() + mBinding.currentFolderName.text = project.description + mBinding.currentFolderName.show() + } else { + mBinding.currentFolderIcon.cloak() + mBinding.currentFolderName.cloak() + } } refreshCurrentFolderCount() @@ -351,16 +355,18 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener private fun refreshCurrentFolderCount() { val project = getSelectedProject() - if (project != null) { - mBinding.currentFolderCount.text = NumberFormat.getInstance().format( - project.collections.map { it.size } - .reduceOrNull { acc, count -> acc + count } ?: 0) - mBinding.currentFolderCount.show() + MainScope().launch { + if (project != null) { + mBinding.currentFolderCount.text = NumberFormat.getInstance().format( + project.collections.map { it.size } + .reduceOrNull { acc, count -> acc + count } ?: 0) + mBinding.currentFolderCount.show() - mBinding.uploadEditButton.toggle(project.isUploading) - } else { - mBinding.currentFolderCount.cloak() - mBinding.uploadEditButton.hide() + mBinding.uploadEditButton.toggle(project.isUploading) + } else { + mBinding.currentFolderCount.cloak() + mBinding.uploadEditButton.hide() + } } } 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 51560572..2d6d8480 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 @@ -71,13 +71,13 @@ class MainMediaFragment : Fragment() { setHasOptionsMenu(true) } - override fun onStart() { - super.onStart() + override fun onResume() { + super.onResume() BroadcastManager.register(requireContext(), mMessageReceiver) } - override fun onStop() { - super.onStop() + override fun onPause() { + super.onPause() BroadcastManager.unregister(requireContext(), mMessageReceiver) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt index 42912156..e82a04ce 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt @@ -35,9 +35,9 @@ class SaveClient(context: Context) : StrongBuilderBase okBuilder = OkHttpClient.Builder() .addInterceptor(cacheInterceptor) - .connectTimeout(20L, TimeUnit.SECONDS) - .writeTimeout(20L, TimeUnit.SECONDS) - .readTimeout(20L, TimeUnit.SECONDS) + .connectTimeout(60L, TimeUnit.SECONDS) + .writeTimeout(60L, TimeUnit.SECONDS) + .readTimeout(60L, TimeUnit.SECONDS) .retryOnConnectionFailure(false) .protocols(arrayListOf(Protocol.HTTP_1_1)) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt index 1b3322d6..916a99c0 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt @@ -9,7 +9,6 @@ import net.opendasharchive.openarchive.services.Conduit import net.opendasharchive.openarchive.services.SaveClient import okhttp3.* import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okio.BufferedSink import java.io.File import java.io.IOException @@ -68,15 +67,7 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { @Throws(IOException::class) private suspend fun uploadMetaData(content: String, basePath: String, fileName: String) { - val requestBody = object : RequestBody() { - override fun contentType(): MediaType? { - return "texts".toMediaTypeOrNull() - } - - override fun writeTo(sink: BufferedSink) { - sink.writeString(content, Charsets.UTF_8) - } - } + val requestBody = RequestBodyUtil.create(content) put( "$ARCHIVE_API_ENDPOINT/$basePath/$fileName.meta.json", @@ -91,7 +82,6 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { val requestBody = getRequestBodyMetaData( uploadFile, Uri.fromFile(uploadFile).toString(), - "texts".toMediaTypeOrNull() ) put("$ARCHIVE_API_ENDPOINT/$basePath/${uploadFile.name}", @@ -127,7 +117,7 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { } /// request body for meta data - private fun getRequestBodyMetaData(media: File, mediaUri: String, mediaType: MediaType?): RequestBody { + private fun getRequestBodyMetaData(media: File, mediaUri: String, mediaType: MediaType? = "texts".toMediaTypeOrNull()): RequestBody { return RequestBodyUtil.create( mContext.contentResolver, Uri.parse(mediaUri), @@ -256,4 +246,4 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { } }) } -} \ No newline at end of file +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt index 352200d2..1635f1e5 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt @@ -1,14 +1,14 @@ package net.opendasharchive.openarchive.services.internetarchive -import okio.source -import okhttp3.internal.closeQuietly -import okhttp3.RequestBody -import kotlin.Throws -import okio.BufferedSink import android.content.ContentResolver import android.net.Uri import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody +import okhttp3.internal.closeQuietly +import okio.BufferedSink import okio.Source +import okio.source import timber.log.Timber import java.io.* @@ -145,4 +145,20 @@ object RequestBodyUtil { } } } -} \ No newline at end of file + + fun create(content: String, mediaType: MediaType? = "texts".toMediaTypeOrNull()): RequestBody { + return object : RequestBody() { + override fun contentType(): MediaType? { + return mediaType + } + + override fun contentLength(): Long { + return content.length.toLong() + } + + override fun writeTo(sink: BufferedSink) { + sink.writeString(content, Charsets.UTF_8) + } + } + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt index 7d3ec687..08f92adf 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt @@ -30,7 +30,7 @@ object BroadcastManager { } fun getAction(intent: Intent): Action? { - val action = Action.values().firstOrNull { it.id == intent.action } + val action = Action.entries.firstOrNull { it.id == intent.action } action?.mediaId = intent.getLongExtra(MEDIA_ID, -1) return action @@ -47,4 +47,4 @@ object BroadcastManager { fun unregister(context: Context, receiver: BroadcastReceiver) { LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver) } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/fragment_space_setup_success.xml b/app/src/main/res/layout/fragment_space_setup_success.xml index b9619941..9413ef00 100644 --- a/app/src/main/res/layout/fragment_space_setup_success.xml +++ b/app/src/main/res/layout/fragment_space_setup_success.xml @@ -49,14 +49,16 @@