diff --git a/.github/workflows/detekt.yml b/.github/workflows/detekt.yml
index 974e48a9..20010067 100644
--- a/.github/workflows/detekt.yml
+++ b/.github/workflows/detekt.yml
@@ -1,116 +1,66 @@
-# This workflow uses actions that are not certified by GitHub.
-# They are provided by a third-party and are governed by
-# separate terms of service, privacy policy, and support
-# documentation.
-
-# This workflow performs a static analysis of your Kotlin source code using
-# Detekt.
-#
-# Scans are triggered:
-# 1. On every push to default and protected branches
-# 2. On every Pull Request targeting the default branch
-# 3. On a weekly schedule
-# 4. Manually, on demand, via the "workflow_dispatch" event
-#
-# The workflow should work with no modifications, but you might like to use a
-# later version of the Detekt CLI by modifying the $DETEKT_RELEASE_TAG
-# environment variable.
-name: Scan with Detekt
+name: Detekt Analysis
on:
- # Triggers the workflow on push or pull request events but only for default and protected branches
+ # Triggers on push to key branches
push:
- branches: [ "master" ]
+ branches: [ "master", "stable", "next", "feature/major-refactor-ui-changes" ]
pull_request:
- branches: [ "master" ]
- schedule:
+ branches:
+ - '**' # Triggers on pull requests to any branch
+ schedule: # Scheduled weekly scan
- cron: '35 5 * * 0'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
-env:
- # Release tag associated with version of Detekt to be installed
- # SARIF support (required for this workflow) was introduced in Detekt v1.15.0
- DETEKT_RELEASE_TAG: v1.22.0
- DETEKT_RELEASE: 1.22.0
-
-# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
- # This workflow contains a single job called "scan"
- scan:
- name: Scan
- # The type of runner that the job will run on
+ detekt:
+ name: Static Code Analysis with Detekt
runs-on: ubuntu-latest
- # Steps represent a sequence of tasks that will be executed as part of the job
steps:
- # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- - uses: actions/checkout@v3
+ # ✅ Step 1: Checkout Repository
+ - name: Check out code
+ uses: actions/checkout@v4
- # Gets the download URL associated with the $DETEKT_RELEASE_TAG
- - name: Get Detekt download URL
- id: detekt_info
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- gh api graphql --field tagName=$DETEKT_RELEASE_TAG --raw-field query='
- query getReleaseAssetDownloadUrl($tagName: String!) {
- repository(name: "detekt", owner: "detekt") {
- release(tagName: $tagName) {
- releaseAssets(name: "detekt", first: 1) {
- nodes {
- downloadUrl
- }
- }
- tagCommit {
- oid
- }
- }
- }
- }
- ' 1> gh_response.json
+ # ✅ Step 2: Set up Java 17
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '17'
- DETEKT_RELEASE_SHA=$(jq --raw-output '.data.repository.release.releaseAssets' gh_response.json)
- if [ $DETEKT_RELEASE_SHA != "4b1da0d5feb53d9ae9b80193ad49c5597d7c4b42" ]; then
- echo "Release tag doesn't match expected commit SHA"
- exit 1
- fi
- cat gh_response.json
- DETEKT_DOWNLOAD_URL=https://github.com/detekt/detekt/releases/download/$DETEKT_RELEASE_TAG/detekt-cli-$DETEKT_RELEASE-all.jar
- echo $DETEKT_DOWNLOAD_URL
- echo "download_url=$DETEKT_DOWNLOAD_URL" >> $GITHUB_OUTPUT
+ # ✅ Step 3: Install Ruby and Bundler (Required for Fastlane)
+ - name: Install Ruby and Bundler
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.3'
+ bundler-cache: true
- # Sets up and runs the detekt cli
- - name: Setup and Run Detekt
- continue-on-error: true
- id: detekt_setup_and_run
+ # ✅ Step 4: Install Fastlane Dependencies
+ - name: Install Fastlane dependencies
run: |
- curl --request GET \
- --url ${{ steps.detekt_info.outputs.download_url }} \
- --silent \
- --location \
- --output detekt.jar
- chmod a+x detekt.jar
- # Performs static analysis using Detekt
- java -jar "detekt.jar" --input ${{ github.workspace }} --all-rules --report sarif:${{ github.workspace }}/detekt.sarif.json
+ bundle config path vendor/bundle
+ bundle install --jobs 4 --retry 3
- # Modifies the SARIF output produced by Detekt so that absolute URIs are relative
- # This is so we can easily map results onto their source files
- # This can be removed once relative URI support lands in Detekt: https://git.io/JLBbA
- - name: Make artifact location URIs relative
- continue-on-error: true
+ # ✅ Step 5: Run Detekt via Fastlane
+ - name: Run Detekt
run: |
- echo "$(
- jq \
- --arg github_workspace ${{ github.workspace }} \
- '. | ( .runs[].results[].locations[].physicalLocation.artifactLocation.uri |= if test($github_workspace) then .[($github_workspace | length | . + 1):] else . end )' \
- ${{ github.workspace }}/detekt.sarif.json
- )" > ${{ github.workspace }}/detekt.sarif.json
+ bundle exec fastlane detekt
+
+ # ✅ Step 6: Upload SARIF report for GitHub Security Code Scanning
+ - name: Upload SARIF report for GitHub Code Scanning
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: "app/build/reports/detekt/detekt.sarif"
+ category: detekt-analysis
- # Uploads results to GitHub repository using the upload-sarif action
- - uses: github/codeql-action/upload-sarif@v2
+ # ✅ Step 7: Upload Detekt Reports as Artifacts for Download
+ - name: Upload Detekt Reports as Artifacts
+ uses: actions/upload-artifact@v4
with:
- # Path to SARIF file relative to the root of the repository
- sarif_file: ${{ github.workspace }}/detekt.sarif.json
- checkout_path: ${{ github.workspace }}
+ name: detekt-reports
+ path: |
+ */build/reports/detekt/*.html
+ */build/reports/detekt/*.md
+ */build/reports/detekt/*.xml
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 7c0199cf..a070f811 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,3 +55,4 @@ fastlane/.env
/app/release/baselineProfiles/0/save-unspecified-release.dm
/app/release/baselineProfiles/1/save-unspecified-release.dm
/app/release/output-metadata.json
+/app/src/main/assets/.env
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 94e284f5..617f846e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -6,6 +6,8 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose")
id("org.jetbrains.kotlin.plugin.serialization")
id("com.google.devtools.ksp")
+ id("androidx.navigation.safeargs.kotlin")
+ alias(libs.plugins.detekt.plugin)
}
android {
@@ -18,12 +20,12 @@ android {
compileSdk = 34
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = "11"
+ jvmTarget = "17"
}
defaultConfig {
@@ -95,6 +97,14 @@ android {
abortOnError = false
}
+
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+
+
namespace = "net.opendasharchive.openarchive"
configurations.all {
@@ -107,9 +117,15 @@ android {
dependencies {
- val composeVersion = "1.7.7"
+ val composeVersion = "1.7.8"
val material = "1.12.0"
val material3 = "1.3.1"
+ val lifecycle = "2.8.7"
+ val navigation = "2.8.8"
+ val fragment = "1.8.6"
+ val koin = "4.1.0-Beta5"
+
+ val coil = "3.0.4"
// Core Kotlin and Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
@@ -119,23 +135,27 @@ dependencies {
// AndroidX Libraries
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.recyclerview:recyclerview:1.3.2")
+ implementation("androidx.viewpager2:viewpager2:1.1.0")
implementation("androidx.recyclerview:recyclerview-selection:1.1.0")
- implementation("androidx.constraintlayout:constraintlayout:2.2.0")
- implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0")
+ implementation("androidx.constraintlayout:constraintlayout:2.2.1")
+ implementation("androidx.constraintlayout:constraintlayout-compose:1.1.1")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.core:core-splashscreen:1.0.1")
- implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.7")
- implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
- implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
- implementation("androidx.navigation:navigation-fragment-ktx:2.8.6")
- implementation("androidx.navigation:navigation-ui-ktx:2.8.6")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle")
+ implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle")
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycle")
+
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.biometric:biometric:1.1.0")
implementation("androidx.work:work-runtime-ktx:2.9.1")
implementation("androidx.security:security-crypto-ktx:1.1.0-alpha06")
+ implementation("androidx.fragment:fragment-ktx:$fragment")
+ implementation("androidx.fragment:fragment-compose:$fragment")
+
// Compose Preferences
implementation("me.zhanghai.compose.preference:library:1.1.1")
@@ -143,9 +163,10 @@ dependencies {
implementation("com.google.android.material:material:$material")
// AndroidX SwipeRefreshLayout
- implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
+ implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
// Compose Libraries
+ implementation("androidx.activity:activity-ktx:1.9.3")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.compose.material3:material3:$material3")
implementation("androidx.compose.ui:ui:$composeVersion")
@@ -154,24 +175,35 @@ dependencies {
implementation("androidx.compose.material:material-icons-extended:$composeVersion")
debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")
+ implementation("androidx.compose.runtime:runtime:$composeVersion")
+ implementation("androidx.compose.runtime:runtime-livedata:$composeVersion")
+
// Navigation
- implementation("androidx.navigation:navigation-compose:2.8.6")
+ implementation("androidx.navigation:navigation-compose:$navigation")
+ implementation("androidx.navigation:navigation-ui-ktx:$navigation")
+ implementation("androidx.navigation:navigation-fragment-ktx:$navigation")
+ implementation("androidx.navigation:navigation-fragment-compose:$navigation")
// Preference
implementation("androidx.preference:preference-ktx:1.2.1")
// Dependency Injection
- implementation("io.insert-koin:koin-core:4.1.0-Beta5")
- implementation("io.insert-koin:koin-android:4.1.0-Beta5")
- implementation("io.insert-koin:koin-androidx-compose:4.1.0-Beta5")
+ implementation("io.insert-koin:koin-core:$koin")
+ implementation("io.insert-koin:koin-android:$koin")
+ implementation("io.insert-koin:koin-androidx-compose:$koin")
+ implementation("io.insert-koin:koin-androidx-navigation:$koin")
+ implementation("io.insert-koin:koin-compose:$koin")
+ implementation("io.insert-koin:koin-compose-viewmodel:$koin")
+ implementation("io.insert-koin:koin-compose-viewmodel-navigation:$koin")
// Image Libraries
implementation("com.github.bumptech.glide:glide:4.16.0")
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
implementation("com.github.esafirm:android-image-picker:3.0.0")
implementation("com.squareup.picasso:picasso:2.5.2")
- implementation("io.coil-kt:coil-compose:2.7.0")
- implementation("io.coil-kt:coil-video:2.7.0")
+ implementation("io.coil-kt.coil3:coil:$coil")
+ implementation("io.coil-kt.coil3:coil-compose:$coil")
+ implementation("io.coil-kt.coil3:coil-video:$coil")
// Networking and Data
// Networking
@@ -257,20 +289,37 @@ dependencies {
// Tests
testImplementation("junit:junit:4.13.2")
- testImplementation("org.robolectric:robolectric:4.10.3")
+ testImplementation("org.robolectric:robolectric:4.14.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test:runner:1.6.2")
testImplementation("androidx.work:work-testing:2.9.1")
+
+ // Detekt
+ detektPlugins(libs.detekt.formatting)
+ detektPlugins(libs.detekt.rules.authors)
+ detektPlugins(libs.detekt.rules.libraries)
+ detektPlugins(libs.detekt.compose)
}
configurations.all {
exclude(group = "com.google.guava", module = "listenablefuture")
}
+detekt {
+ config.setFrom(file("$rootDir/config/detekt-config.yml"))
+ baseline = file("$rootDir/config/baseline.xml")
+ source.setFrom(
+ files("$rootDir/app/src")
+ )
+ buildUponDefaultConfig = true
+ allRules = false
+ autoCorrect = false
+ ignoreFailures = true
+}
+
/**
testdroid {username '$bbusername'
password '$bbpassword'
deviceGroup 'gpdevices'
mode "FULL_RUN"
-projectName "OASave"}**/
-
+projectName "OASave"}**/
\ No newline at end of file
diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
new file mode 100644
index 00000000..824fccc8
--- /dev/null
+++ b/app/detekt-baseline.xml
@@ -0,0 +1,1925 @@
+
+
+
+
+ AnnotationOnSeparateLine:Hbks.kt$Hbks.Availability.Enroll$@RequiresApi(Build.VERSION_CODES.R) data
+ ArgumentListWrapping:AlertHelper.kt$AlertHelper.Companion$( context, if (message != null) context.getString(message) else null, title, icon, buttons )
+ ArgumentListWrapping:BaseButton.kt$( modifier = modifier, text = text, style = MaterialTheme.typography.bodyLarge.copy( fontSize = fontSize, fontWeight = fontWeight, color = color ))
+ ArgumentListWrapping:BrowseFoldersAdapter.kt$BrowseFoldersAdapter.FolderViewHolder$( binding.root)
+ ArgumentListWrapping:BrowseFoldersAdapter.kt$BrowseFoldersAdapter.FolderViewHolder$(binding.root)
+ ArgumentListWrapping:Collection.kt$Collection.Companion$( Collection::class.java, "project_id = ?", arrayOf(projectId.toString()), null, "id ASC", null)
+ ArgumentListWrapping:Collection.kt$Collection.Companion$(Collection::class.java, "project_id = ?", arrayOf(projectId.toString()), null, "id ASC", null)
+ ArgumentListWrapping:Context.kt$( this, getString(R.string.no_webbrowser_found_error), Toast.LENGTH_LONG)
+ ArgumentListWrapping:Context.kt$(this, getString(R.string.no_webbrowser_found_error), Toast.LENGTH_LONG)
+ ArgumentListWrapping:CreateNewFolderFragment.kt$CreateNewFolderFragment$( requireContext(), getString(R.string.folder_name_already_exists), Toast.LENGTH_LONG )
+ ArgumentListWrapping:Drawable.kt$( TypedValue.COMPLEX_UNIT_DIP, biggerSideDipLength.toFloat(), context.resources.displayMetrics )
+ ArgumentListWrapping:DrawableExtensions.kt$( (intrinsicWidth * factor).roundToInt(), (intrinsicHeight * factor).roundToInt(), context)
+ ArgumentListWrapping:DrawableExtensions.kt$( TypedValue.COMPLEX_UNIT_DIP, biggerSideDipLength.toFloat(), context.resources.displayMetrics)
+ ArgumentListWrapping:DrawableExtensions.kt$((intrinsicWidth * factor).roundToInt(), (intrinsicHeight * factor).roundToInt(), context)
+ ArgumentListWrapping:DrawableExtensions.kt$(TypedValue.COMPLEX_UNIT_DIP, biggerSideDipLength.toFloat(), context.resources.displayMetrics)
+ ArgumentListWrapping:EditFolderActivity.kt$EditFolderActivity$( this, R.string.action_remove_project, R.string.remove_from_app, buttons = listOf( AlertHelper.positiveButton(R.string.remove) { _, _ -> mProject.delete() finish() }, AlertHelper.negativeButton() ) )
+ ArgumentListWrapping:FileUtils.kt$FileUtils$( Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
+ ArgumentListWrapping:FileUtils.kt$FileUtils$("$TAG File -", "Authority: " + uri.authority + ", Fragment: " + uri.fragment + ", Port: " + uri.port + ", Query: " + uri.query + ", Scheme: " + uri.scheme + ", Host: " + uri.host + ", Segments: " + uri.pathSegments.toString() )
+ ArgumentListWrapping:FolderAdapter.kt$FolderAdapter$( LayoutInflater.from(parent.context), parent, false )
+ ArgumentListWrapping:FullscreenDimmingOverlay.kt$FullScreenCreateGroupDimmingOverlay$( context, title = "Confirm", message = "Do you want to cancel?", positiveButtonText = "Yes", negativeButtonText = "No")
+ ArgumentListWrapping:FullscreenDimmingOverlay.kt$FullScreenDimmingOverlay$( context, title = "Confirm", message = "Do you want to cancel?", positiveButtonText = "Yes", negativeButtonText = "No")
+ ArgumentListWrapping:GDriveActivity.kt$GDriveActivity$( AlertHelper.positiveButton(R.string.remove) { _, _ -> // delete sign-in from database space.delete() // google logout val googleSignInClient = GoogleSignIn.getClient(applicationContext, GoogleSignInOptions.DEFAULT_SIGN_IN) googleSignInClient.revokeAccess().addOnCompleteListener { googleSignInClient.signOut() } // leave activity Space.navigate(this) }, AlertHelper.negativeButton())
+ ArgumentListWrapping:GDriveConduit.kt$GDriveConduit$( "the createFolder calls defined in Conduit don't map to GDrive API. use GDriveConduit.createFolder instead")
+ ArgumentListWrapping:GDriveConduit.kt$GDriveConduit$("the createFolder calls defined in Conduit don't map to GDrive API. use GDriveConduit.createFolder instead")
+ ArgumentListWrapping:GDriveConduit.kt$GDriveConduit.Companion$( "mimeType='application/vnd.google-apps.folder' and 'root' in parents and trashed = false")
+ ArgumentListWrapping:GDriveConduit.kt$GDriveConduit.Companion$( "mimeType='application/vnd.google-apps.folder' and name = '$folderName' and trashed = false and '$parentId' in parents")
+ ArgumentListWrapping:GDriveConduit.kt$GDriveConduit.Companion$("mimeType='application/vnd.google-apps.folder' and 'root' in parents and trashed = false")
+ ArgumentListWrapping:GDriveConduit.kt$GDriveConduit.Companion$("mimeType='application/vnd.google-apps.folder' and name = '$folderName' and trashed = false and '$parentId' in parents")
+ ArgumentListWrapping:IaConduit.kt$IaConduit$( mContext.contentResolver, Uri.fromFile(uploadFile), uploadFile.length(), textMediaType, createListener(cancellable = { !mCancelled }) )
+ ArgumentListWrapping:InternetArchiveFragment.kt$InternetArchiveFragment$( message)
+ ArgumentListWrapping:InternetArchiveFragment.kt$InternetArchiveFragment$(message)
+ ArgumentListWrapping:InternetArchiveLoginScreen.kt$( Intent.ACTION_VIEW, Uri.parse(CreateLogin.URI) )
+ ArgumentListWrapping:InternetArchiveLoginScreen.kt$( contract = ActivityResultContracts.StartActivityForResult(), onResult = {})
+ ArgumentListWrapping:InternetArchiveLoginScreen.kt$( modifier = Modifier .weight(1f) .heightIn(ThemeDimensions.touchable) .padding(ThemeDimensions.spacing.small), shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { dispatch(Action.Cancel) })
+ ArgumentListWrapping:InternetArchiveLoginScreen.kt$( modifier = Modifier.heightIn(ThemeDimensions.touchable), onClick = { dispatch(CreateLogin) })
+ ArgumentListWrapping:InternetArchiveLoginScreen.kt$( modifier = Modifier.sizeIn(ThemeDimensions.touchable), onClick = { showPassword = !showPassword })
+ ArgumentListWrapping:InternetArchiveLoginScreen.kt$( username = "user@example.org", password = "abc123" )
+ ArgumentListWrapping:InternetArchiveMapper.kt$InternetArchiveMapper$( access = response.access, secret = response.secret )
+ ArgumentListWrapping:MainActivity.kt$MainActivity$( AddMediaDialogFragment.RESP_FILES, this@MainActivity )
+ ArgumentListWrapping:MainActivity.kt$MainActivity$( AddMediaDialogFragment.RESP_PHOTO_GALLERY, this@MainActivity )
+ ArgumentListWrapping:MainActivity.kt$MainActivity$( AddMediaDialogFragment.RESP_TAKE_PHOTO, this@MainActivity )
+ ArgumentListWrapping:MainActivity.kt$MainActivity$( Context.INPUT_METHOD_SERVICE)
+ ArgumentListWrapping:MainActivity.kt$MainActivity$( Manifest.permission.POST_NOTIFICATIONS)
+ ArgumentListWrapping:MainActivity.kt$MainActivity$(Context.INPUT_METHOD_SERVICE)
+ ArgumentListWrapping:MainActivity.kt$MainActivity$(Manifest.permission.POST_NOTIFICATIONS)
+ ArgumentListWrapping:Media.kt$Media.Companion$( Media::class.java, statuses.joinToString(" OR ") { "status = ?" }, statuses.map { it.id.toString() }.toTypedArray(), null, order, null )
+ ArgumentListWrapping:MediaAdapter.kt$MediaAdapter$( it, it.getString(R.string.upload_unsuccessful_description), R.string.upload_unsuccessful, R.drawable.ic_error, listOf( AlertHelper.positiveButton(R.string.retry) { _, _ -> media[pos].apply { sStatus = Media.Status.Queued statusMessage = "" save() BroadcastManager.postChange(it, collectionId, id) } UploadService.startUploadService(it) }, AlertHelper.negativeButton(R.string.remove) { _, _ -> deleteItem(pos) }, AlertHelper.neutralButton() ) )
+ ArgumentListWrapping:MediaViewHolder.kt$MediaViewHolder$( "Binding media item ${media?.id} with status ${media?.sStatus} and progress ${media?.uploadPercentage}")
+ ArgumentListWrapping:MediaViewHolder.kt$MediaViewHolder$("Binding media item ${media?.id} with status ${media?.sStatus} and progress ${media?.uploadPercentage}")
+ ArgumentListWrapping:Onboarding23InstructionsActivity.kt$Onboarding23InstructionsActivity$( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE )
+ ArgumentListWrapping:Onboarding23InstructionsActivity.kt$Onboarding23InstructionsActivity.<no name provided>$( mBinding.fab.context, R.drawable.ic_arrow_right, )
+ ArgumentListWrapping:Onboarding23InstructionsActivity.kt$Onboarding23InstructionsActivity.<no name provided>$( mBinding.fab.context, com.esafirm.imagepicker.R.drawable.ef_ic_done_white, )
+ ArgumentListWrapping:PasscodeEntryScreen.kt$( text = "Enter Your Passcode", style = TextStyle( fontSize = 18.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground ) )
+ ArgumentListWrapping:Picker.kt$Picker$( Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO)
+ ArgumentListWrapping:Picker.kt$Picker$( context, "${context.packageName}.provider", it )
+ ArgumentListWrapping:ProofModeScreen.kt$( stringResource( R.string.prefs_use_proofmode_description, "https://www.google.com" ), HtmlCompat.FROM_HTML_MODE_COMPACT )
+ ArgumentListWrapping:ProofModeSettingsActivity.kt$ProofModeSettingsActivity.Fragment$( R.string.pref_key_use_proof_mode)
+ ArgumentListWrapping:ProofModeSettingsActivity.kt$ProofModeSettingsActivity.Fragment$(R.string.pref_key_use_proof_mode)
+ ArgumentListWrapping:SaveClient.kt$SaveClient.Companion.<no name provided>$( Result.failure(OrbotException(context.getString(R.string.tor_connection_invalid))))
+ ArgumentListWrapping:SaveClient.kt$SaveClient.Companion.<no name provided>$( Result.failure(OrbotException(context.getString(R.string.tor_connection_timeout))))
+ ArgumentListWrapping:SaveClient.kt$SaveClient.Companion.<no name provided>$( Result.failure(e ?: OrbotException(context.getString(R.string.tor_connection_exception))))
+ ArgumentListWrapping:SaveClient.kt$SaveClient.Companion.<no name provided>$(Result.failure(OrbotException(context.getString(R.string.tor_connection_invalid))))
+ ArgumentListWrapping:SaveClient.kt$SaveClient.Companion.<no name provided>$(Result.failure(OrbotException(context.getString(R.string.tor_connection_timeout))))
+ ArgumentListWrapping:SaveClient.kt$SaveClient.Companion.<no name provided>$(Result.failure(e ?: OrbotException(context.getString(R.string.tor_connection_exception))))
+ ArgumentListWrapping:SettingsScreen.kt$( "light" to "Light", "dark" to "Dark", "system" to "System Default" )
+ ArgumentListWrapping:SettingsScreen.kt$( key = "about_app", title = { Text("Save by Open Archive") }, summary = { Text("Tap to view about Save App") }, onClick = { // Handle URL intent openUrl(context, "https://open-archive.org/save") })
+ ArgumentListWrapping:SettingsScreen.kt$( key = "pref_app_passcode", defaultValue = false, title = { Text("Lock app with passcode") }, summary = { Text("6 digit passcode") })
+ ArgumentListWrapping:SettingsScreen.kt$( key = "pref_media_folders", title = { Text("Media Folders") }, summary = { Text("Add or remove media folders") })
+ ArgumentListWrapping:SettingsScreen.kt$( key = "pref_media_servers", title = { Text("Media Servers") }, summary = { Text("Add or remove media servers") })
+ ArgumentListWrapping:SettingsScreen.kt$( key = "privacy_policy", title = { Text("Terms & Privacy Policy") }, summary = { Text("Tap to view our Terms & Privacy Policy") }, onClick = { // Handle URL intent openUrl(context, "https://open-archive.org/privacy") })
+ ArgumentListWrapping:SettingsScreen.kt$( key = "proof_mode", title = { Text("Proof Mode") })
+ ArgumentListWrapping:SettingsScreen.kt$( key = "upload_wifi_only", defaultValue = false, title = { Text("Upload over Wi-Fi only") }, summary = { Text("Only upload media when connected to Wi-Fi") })
+ ArgumentListWrapping:SettingsScreen.kt$( key = "use_tor", defaultValue = false, title = { Text("Use Tor") }, summary = { Text("Enable Tor for encryption") })
+ ArgumentListWrapping:SmartFragmentStatePagerAdapter.kt$SmartFragmentStatePagerAdapter$( fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
+ ArgumentListWrapping:SmartFragmentStatePagerAdapter.kt$SmartFragmentStatePagerAdapter$(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
+ ArgumentListWrapping:SnowbirdCreateGroupFragment.kt$SnowbirdCreateGroupFragment$( group.key, viewBinding.repoNameTextfield.text.toString() )
+ ArgumentListWrapping:SnowbirdFileListAdapter.kt$SnowbirdFileListAdapter$( ContextCompat.getDrawable(context, R.drawable.outline_cloud_done_24)?.scaled(40, context))
+ ArgumentListWrapping:SnowbirdFileListAdapter.kt$SnowbirdFileListAdapter$( ContextCompat.getDrawable(context, R.drawable.outline_cloud_download_24)?.scaled(40, context))
+ ArgumentListWrapping:SnowbirdFileListAdapter.kt$SnowbirdFileListAdapter$(ContextCompat.getDrawable(context, R.drawable.outline_cloud_done_24)?.scaled(40, context))
+ ArgumentListWrapping:SnowbirdFileListAdapter.kt$SnowbirdFileListAdapter$(ContextCompat.getDrawable(context, R.drawable.outline_cloud_download_24)?.scaled(40, context))
+ ArgumentListWrapping:SnowbirdFileListFragment.kt$SnowbirdFileListFragment$( ActivityResultContracts.GetMultipleContents())
+ ArgumentListWrapping:SnowbirdFileListFragment.kt$SnowbirdFileListFragment$( R.color.colorPrimary, R.color.colorPrimaryDark )
+ ArgumentListWrapping:SnowbirdFileListFragment.kt$SnowbirdFileListFragment$( requireContext(), title = "Download Media?", message = "Are you sure you want to download this media?", positiveButtonText = "Yes", negativeButtonText = "No")
+ ArgumentListWrapping:SnowbirdFileListFragment.kt$SnowbirdFileListFragment$( requireContext(), title = "Success", message = "File successfully downloaded")
+ ArgumentListWrapping:SnowbirdFileListFragment.kt$SnowbirdFileListFragment$(ActivityResultContracts.GetMultipleContents())
+ ArgumentListWrapping:SnowbirdGroup.kt$SnowbirdGroup.Companion$( SnowbirdGroup::class.java, whereClause, whereArgs.toTypedArray(), null, null, null)
+ ArgumentListWrapping:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment$( RESULT_REQUEST_KEY, bundleOf( RESULT_BUNDLE_NAVIGATION_KEY to RESULT_VAL_RAVEN_REPO_LIST_SCREEN, RESULT_BUNDLE_GROUP_KEY to groupKey ) )
+ ArgumentListWrapping:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment$( groupKey)
+ ArgumentListWrapping:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment$(groupKey)
+ ArgumentListWrapping:SnowbirdRepo.kt$SnowbirdRepo.Companion$( SnowbirdRepo::class.java, whereClause, whereArgs.toTypedArray(), null, null, null )
+ ArgumentListWrapping:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment$( R.color.colorPrimary, R.color.colorPrimaryDark )
+ ArgumentListWrapping:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment$( object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.menu_snowbird, menu) } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { return when (menuItem.itemId) { R.id.action_add -> { Utility.showMaterialWarning( context = requireContext(), message = "Feature not implemented yet.", positiveButtonText = "OK" ) true } else -> false } } }, viewLifecycleOwner, Lifecycle.State.RESUMED )
+ ArgumentListWrapping:Space.kt$Space.Companion$( Space::class.java, whereClause, whereArgs.toTypedArray(), null, null, null )
+ ArgumentListWrapping:SpaceAdapter.kt$SpaceAdapter$( DIFF_CALLBACK)
+ ArgumentListWrapping:SpaceAdapter.kt$SpaceAdapter$( LayoutInflater.from(parent.context), parent, false )
+ ArgumentListWrapping:SpaceAdapter.kt$SpaceAdapter$(DIFF_CALLBACK)
+ ArgumentListWrapping:SpaceDrawerAdapter.kt$SpaceDrawerAdapter$( DIFF_CALLBACK)
+ ArgumentListWrapping:SpaceDrawerAdapter.kt$SpaceDrawerAdapter$(DIFF_CALLBACK)
+ ArgumentListWrapping:SwipeToDeleteCallback.kt$SwipeToDeleteCallback$( ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START)
+ ArgumentListWrapping:SwipeToDeleteCallback.kt$SwipeToDeleteCallback$(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START)
+ ArgumentListWrapping:TextView.kt$( Position.Start.get(drawables), Position.Top.get(drawables), Position.End.get(drawables), Position.Bottom.get(drawables))
+ ArgumentListWrapping:TorStatusDatabase.kt$TorStatusDatabase$( context, DATABASE_NAME, null, DATABASE_VERSION)
+ ArgumentListWrapping:TorStatusDatabase.kt$TorStatusDatabase$(context, DATABASE_NAME, null, DATABASE_VERSION)
+ ArgumentListWrapping:UnixSocketClient.kt$UnixSocketClient$( endpoint, method, body, { json.encodeToString(it) }, { json.decodeFromString<RESPONSE>(it) })
+ ArgumentListWrapping:UnixSocketClient.kt$UnixSocketClient$( socket, endpoint, method, body, serialize)
+ ArgumentListWrapping:UnixSocketClient.kt$UnixSocketClient$(endpoint, method, body, { json.encodeToString(it) }, { json.decodeFromString<RESPONSE>(it) })
+ ArgumentListWrapping:UnixSocketClient.kt$UnixSocketClient$(socket, endpoint, method, body, serialize)
+ ArgumentListWrapping:UploadService.kt$UploadService$( NOTIFICATION_CHANNEL_ID, getString(R.string.uploads), NotificationManager.IMPORTANCE_LOW )
+ ArgumentListWrapping:UploadService.kt$UploadService$( this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE )
+ ArgumentListWrapping:WebDavConduit.kt$WebDavConduit$( chunkPath, buffer, mMedia.mimeType, object : SardineListener { override fun transferred(bytes: Long) { jobProgress(offset.toLong() + bytes) } override fun continueUpload(): Boolean { return !mCancelled } })
+ ArgumentListWrapping:WebDavConduit.kt$WebDavConduit$( construct(base, path, file.name), file, "text/plain", false, null)
+ ArgumentListWrapping:WebDavConduit.kt$WebDavConduit$( mContext.contentResolver, fullPath, mMedia.fileUri, mMedia.contentLength, mMedia.mimeType, false, object : SardineListener { var lastBytes: Long = 0 override fun transferred(bytes: Long) { if (bytes > lastBytes) { jobProgress(bytes) lastBytes = bytes } AppLogger.i("Bytes transferred for for ${mMedia.id}: ", "$bytes") } override fun continueUpload(): Boolean { AppLogger.i("Should continue upload for ${mMedia.id}?", "$mCancelled") return !mCancelled } })
+ ArgumentListWrapping:WebDavConduit.kt$WebDavConduit$(mContext.contentResolver, fullPath, mMedia.fileUri, mMedia.contentLength, mMedia.mimeType, false, object : SardineListener { var lastBytes: Long = 0 override fun transferred(bytes: Long) { if (bytes > lastBytes) { jobProgress(bytes) lastBytes = bytes } AppLogger.i("Bytes transferred for for ${mMedia.id}: ", "$bytes") } override fun continueUpload(): Boolean { AppLogger.i("Should continue upload for ${mMedia.id}?", "$mCancelled") return !mCancelled } })
+ ArgumentListWrapping:WebDavSetupLicenseFragment.kt$WebDavSetupLicenseFragment$( message = getString(R.string.you_have_successfully_connected_to_a_private_server))
+ ArgumentListWrapping:WebDavSetupLicenseFragment.kt$WebDavSetupLicenseFragment$(message = getString(R.string.you_have_successfully_connected_to_a_private_server))
+ ChainWrapping:Media.kt$Media$||
+ ChainWrapping:Picker.kt$Picker$&&
+ ChainWrapping:PreviewAdapter.kt$PreviewAdapter.Companion.<no name provided>$&&
+ CommentSpacing:AddFolderActivity.kt$AddFolderActivity$//mBinding = ActivityAddFolderBinding.inflate(layoutInflater)
+ CommentSpacing:AddFolderActivity.kt$AddFolderActivity$//mBinding.browseFolderContainer.hide()
+ CommentSpacing:AddFolderActivity.kt$AddFolderActivity$//setContentView(mBinding.root)
+ CommentSpacing:BadgeDrawable.kt$BadgeDrawable$//NO-OP
+ CommentSpacing:BaseSnowbirdFragment.kt$BaseSnowbirdFragment$//FullScreenOverlayManager.hide()
+ CommentSpacing:BaseSnowbirdFragment.kt$BaseSnowbirdFragment$//FullScreenOverlayManager.show(this@BaseSnowbirdFragment)
+ CommentSpacing:DialogConfigBuilder.kt$DialogBuilder$//?: ButtonData(defaultPositiveTextFor(type)),
+ CommentSpacing:HomeActivity.kt$HomeActivity$//TODO: Refresh projects in MainViewModel
+ CommentSpacing:HomeScreen.kt$//@Composable
+ CommentSpacing:HomeScreen.kt$//fun MainMediaScreen(projectId: Long) {
+ CommentSpacing:HomeScreen.kt$//}
+ CommentSpacing:IaConduit.kt$IaConduit$/// Upload ProofMode metadata, if enabled and successfully created.
+ CommentSpacing:IaConduit.kt$IaConduit$/// headers for meta-data and proof mode
+ CommentSpacing:IaConduit.kt$IaConduit$/// upload proof mode
+ CommentSpacing:InternetArchiveActivity.kt$//fun Activity.measureNewBackend(type: Space.Type) {
+ CommentSpacing:InternetArchiveActivity.kt$//}
+ CommentSpacing:InternetArchiveDetailsScreen.kt$//InternetArchiveHeader()
+ CommentSpacing:InternetArchiveDetailsScreen.kt$//dismiss
+ CommentSpacing:InternetArchiveDetailsScreen.kt$//isRemoving = true
+ CommentSpacing:InternetArchiveLoginScreen.kt$//focusedIndicatorColor = Color.Transparent,
+ CommentSpacing:InternetArchiveLoginScreen.kt$//unfocusedIndicatorColor = Color.Transparent,
+ CommentSpacing:MainActivity.kt$MainActivity$///enableEdgeToEdge()
+ CommentSpacing:MainActivity.kt$MainActivity$//binding.contentMain.tvSelectedCount.text = if (count > 0) "Selected: $count" else "Select Media"
+ CommentSpacing:MainMediaFragment.kt$MainMediaFragment$//update selection UI by summing selected counts from all adapters.
+ CommentSpacing:MediaAdapter.kt$MediaAdapter$//CleanInsightsManager.measureEvent("backend", "upload-error", media[pos].space?.friendlyName)
+ CommentSpacing:MediaViewHolder.kt$MediaViewHolder.Box$//(binding as RvMediaBoxBinding).fileInfo
+ CommentSpacing:MediaViewHolder.kt$MediaViewHolder.Box$//(binding as RvMediaBoxBinding).title
+ CommentSpacing:PasscodeSetupActivity.kt$PasscodeSetupActivity$//onBackPressedCallback.handleOnBackPressed()
+ CommentSpacing:PasscodeSetupActivity.kt$PasscodeSetupActivity$//onBackPressedDispatcher.addCallback(onBackPressedCallback)
+ CommentSpacing:PreviewActivity.kt$PreviewActivity$//mBinding.addMenu.container.show(animate = true)
+ CommentSpacing:SettingsFragment.kt$SettingsFragment$//torViewModel.updateTorServiceState()
+ CommentSpacing:SnowbirdFileListAdapter.kt$SnowbirdFileListAdapter$//button.setBackgroundResource(R.drawable.button_outlined_ripple)
+ CommentSpacing:SnowbirdGroupListAdapter.kt$//interface SnowbirdGroupsAdapterListener {
+ CommentSpacing:SnowbirdGroupListAdapter.kt$//}
+ CommentSpacing:SnowbirdGroupListAdapter.kt$SnowbirdGroupsAdapter.ViewHolder$//binding.button.setBackgroundResource(R.drawable.button_outlined_ripple)
+ CommentSpacing:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment$//findNavController().navigate(SnowbirdGroupListFragmentDirections.navigateToSnowbirdShareScreen(groupKey))
+ CommentSpacing:SnowbirdRepoListAdapter.kt$SnowbirdRepoListAdapter.SnowbirdRepoListViewHolder$//binding.button.setBackgroundResource(R.drawable.button_outlined_ripple)
+ CommentSpacing:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment$//findNavController().navigate(SnowbirdRepoListFragmentDirections.navigateToSnowbirdListFilesScreen(groupKey, repoKey))
+ CommentSpacing:SpaceAdapter.kt$SpaceAdapter$//@Suppress("NAME_SHADOWING")
+ CommentSpacing:SpaceAdapter.kt$SpaceAdapter$//spaces.add(Space(ADD_SPACE_ID))
+ CommentSpacing:SpaceAdapter.kt$SpaceAdapter$//val spaces = spaces.toMutableList()
+ CommentSpacing:UnixSocketClient.kt$//sealed class ClientResponse<out T> {
+ CommentSpacing:UnixSocketClient.kt$//}
+ CommentSpacing:WebDavConduit.kt$WebDavConduit$/// Upload ProofMode metadata, if enabled and successfully created.
+ CommentSpacing:WebDavFragment.kt$WebDavFragment$//Refresh menu to hide confirm btn again
+ CommentSpacing:WebDavFragment.kt$WebDavFragment$//attemptLogin()
+ CommentSpacing:WebDavFragment.kt$WebDavFragment.<no name provided>$//todo: save changes here and show success dialog
+ CommentWrapping:MainMediaScreen.kt$/* no op */
+ ComplexCondition:Hbks.kt$Hbks$key == null || cipher == null || ciphertext == null || ciphertext.size < 12
+ ComposableParamOrder:Accordion.kt$Accordion
+ ComposableParamOrder:BaseDialog.kt$BaseDialog
+ ComposableParamOrder:ExpandableSpaceList.kt$ExpandableSpaceList
+ ComposableParamOrder:FolderOptionsPopup.kt$FolderOptionsPopup
+ ComposableParamOrder:HomeScreen.kt$HomeScreen
+ ComposableParamOrder:HomeScreen.kt$SaveNavGraph
+ ComposableParamOrder:InternetArchiveLoginScreen.kt$CustomSecureField
+ ComposableParamOrder:InternetArchiveLoginScreen.kt$CustomTextField
+ ComposableParamOrder:NumericKeypad.kt$NumberButton
+ ComposableParamOrder:NumericKeypad.kt$NumericKeypad
+ ComposableParamOrder:PrimaryButton.kt$PrimaryButton
+ ComposableParamOrder:UiImage.kt$UiImage$asIcon
+ CompositionLocalAllowlist:Colors.kt$LocalColors
+ CompositionLocalAllowlist:Dimensions.kt$LocalDimensions
+ ContentSlotReused:Accordion.kt$bodyContent
+ CyclomaticComplexMethod:DialogConfigBuilder.kt$DialogBuilder$@Composable fun build(): DialogConfig
+ CyclomaticComplexMethod:DialogConfigBuilder.kt$DialogBuilder$fun build(resourceProvider: ResourceProvider): DialogConfig
+ CyclomaticComplexMethod:FileUtils.kt$FileUtils$@SuppressLint("NewAPI", "LogNotTimber") fun getPath(context: Context, uri: Uri): String?
+ CyclomaticComplexMethod:HomeScreen.kt$@Composable fun HomeScreenContent( onExit: () -> Unit, state: HomeScreenState, onAction: (HomeScreenAction) -> Unit, onNavigateToCache: () -> Unit = {} )
+ CyclomaticComplexMethod:IaConduit.kt$IaConduit$private fun mainHeader(): Headers
+ CyclomaticComplexMethod:MainMediaViewHolder.kt$MainMediaViewHolder$fun bind(media: Media? = null, isInSelectionMode: Boolean = false, doImageFade: Boolean = true)
+ CyclomaticComplexMethod:MediaAdapter.kt$MediaAdapter$override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder
+ CyclomaticComplexMethod:MediaViewHolder.kt$MediaViewHolder$@SuppressLint("SetTextI18n") fun bind(media: Media? = null, batchMode: Boolean = false, doImageFade: Boolean = true)
+ CyclomaticComplexMethod:NumericKeypad.kt$@Composable private fun NumberButton( label: String, enabled: Boolean = true, onClick: () -> Unit, hapticManager: HapticManager = koinInject() )
+ CyclomaticComplexMethod:PreviewViewHolder.kt$PreviewViewHolder$@SuppressLint("SetTextI18n") fun bind(media: Media? = null, batchMode: Boolean = false, doImageFade: Boolean = true)
+ CyclomaticComplexMethod:ReviewActivity.kt$ReviewActivity$private fun refresh()
+ CyclomaticComplexMethod:UnixSocketClientUtilityExtensions.kt$suspend fun UnixSocketClient.readBinaryResponseWithCancellation( inputStream: InputStream, onProgress: ((Long) -> Unit)? = null ): Triple<Int, Map<String, String>, ByteArray>
+ CyclomaticComplexMethod:WebDavConduit.kt$WebDavConduit$@Throws(IOException::class) private suspend fun uploadChunked(base: HttpUrl, path: List<String>, fileName: String): Boolean
+ EmptyFunctionBlock:CreateNewFolderFragment.kt$CreateNewFolderFragment.<no name provided>${}
+ EmptyFunctionBlock:PasscodeEntryViewModel.kt$PasscodeEntryViewModel${ }
+ EmptyFunctionBlock:ReviewActivity.kt$ReviewActivity.<no name provided>${ }
+ EmptyFunctionBlock:TorStatusDatabase.kt$TorStatusDatabase${ }
+ EmptyFunctionBlock:WebDavFragment.kt$WebDavFragment.<no name provided>${}
+ Filename:SnowbirdGroupListAdapter.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdGroupListAdapter.kt
+ FinalNewline:ActivityExtension.kt$net.opendasharchive.openarchive.extensions.ActivityExtension.kt
+ FinalNewline:AddFolderActivity.kt$net.opendasharchive.openarchive.features.folders.AddFolderActivity.kt
+ FinalNewline:AddFolderScreen.kt$net.opendasharchive.openarchive.features.folders.AddFolderScreen.kt
+ FinalNewline:AddMediaDialogFragment.kt$net.opendasharchive.openarchive.features.media.AddMediaDialogFragment.kt
+ FinalNewline:AddMediaType.kt$net.opendasharchive.openarchive.features.media.AddMediaType.kt
+ FinalNewline:AlertHelper.kt$net.opendasharchive.openarchive.util.AlertHelper.kt
+ FinalNewline:ApiError.kt$net.opendasharchive.openarchive.db.ApiError.kt
+ FinalNewline:ApiResponse.kt$net.opendasharchive.openarchive.services.snowbird.service.ApiResponse.kt
+ FinalNewline:AppConfig.kt$net.opendasharchive.openarchive.features.settings.passcode.AppConfig.kt
+ FinalNewline:AppLogger.kt$net.opendasharchive.openarchive.core.logger.AppLogger.kt
+ FinalNewline:ApplicationExtensions.kt$net.opendasharchive.openarchive.extensions.ApplicationExtensions.kt
+ FinalNewline:BackoffStrategy.kt$net.opendasharchive.openarchive.services.snowbird.service.BackoffStrategy.kt
+ FinalNewline:BadgeDrawable.kt$net.opendasharchive.openarchive.util.BadgeDrawable.kt
+ FinalNewline:BaseActivity.kt$net.opendasharchive.openarchive.features.core.BaseActivity.kt
+ FinalNewline:BaseButton.kt$net.opendasharchive.openarchive.features.core.BaseButton.kt
+ FinalNewline:BaseComposeActivity.kt$net.opendasharchive.openarchive.features.core.BaseComposeActivity.kt
+ FinalNewline:BaseDialog.kt$net.opendasharchive.openarchive.features.core.dialog.BaseDialog.kt
+ FinalNewline:BaseFragment.kt$net.opendasharchive.openarchive.features.core.BaseFragment.kt
+ FinalNewline:BaseSnowbirdFragment.kt$net.opendasharchive.openarchive.services.snowbird.BaseSnowbirdFragment.kt
+ FinalNewline:BaseViewModel.kt$net.opendasharchive.openarchive.util.BaseViewModel.kt
+ FinalNewline:BasicAuthInterceptor.kt$net.opendasharchive.openarchive.services.webdav.BasicAuthInterceptor.kt
+ FinalNewline:BiometricAuthenticator.kt$net.opendasharchive.openarchive.features.settings.passcode.BiometricAuthenticator.kt
+ FinalNewline:BottomSheetExtensions.kt$net.opendasharchive.openarchive.extensions.BottomSheetExtensions.kt
+ FinalNewline:BrowseFolderScreen.kt$net.opendasharchive.openarchive.features.folders.BrowseFolderScreen.kt
+ FinalNewline:BrowseFoldersAdapter.kt$net.opendasharchive.openarchive.features.folders.BrowseFoldersAdapter.kt
+ FinalNewline:BrowseFoldersFragment.kt$net.opendasharchive.openarchive.features.folders.BrowseFoldersFragment.kt
+ FinalNewline:BrowseFoldersViewModel.kt$net.opendasharchive.openarchive.features.folders.BrowseFoldersViewModel.kt
+ FinalNewline:Collection.kt$net.opendasharchive.openarchive.db.Collection.kt
+ FinalNewline:Colors.kt$net.opendasharchive.openarchive.core.presentation.theme.Colors.kt
+ FinalNewline:Conduit.kt$net.opendasharchive.openarchive.services.Conduit.kt
+ FinalNewline:ConsentActivity.kt$net.opendasharchive.openarchive.features.settings.ConsentActivity.kt
+ FinalNewline:ContentPickerFragment.kt$net.opendasharchive.openarchive.features.media.ContentPickerFragment.kt
+ FinalNewline:Context.kt$net.opendasharchive.openarchive.util.extensions.Context.kt
+ FinalNewline:CreativeCommonsLicenseManager.kt$net.opendasharchive.openarchive.features.settings.CreativeCommonsLicenseManager.kt
+ FinalNewline:CustomBottomNavBar.kt$net.opendasharchive.openarchive.core.presentation.components.CustomBottomNavBar.kt
+ FinalNewline:CustomButton.kt$net.opendasharchive.openarchive.features.main.ui.CustomButton.kt
+ FinalNewline:DefaultScaffold.kt$net.opendasharchive.openarchive.features.settings.passcode.components.DefaultScaffold.kt
+ FinalNewline:DialogConfigBuilder.kt$net.opendasharchive.openarchive.features.core.dialog.DialogConfigBuilder.kt
+ FinalNewline:Drawable.kt$net.opendasharchive.openarchive.util.extensions.Drawable.kt
+ FinalNewline:DrawableExtensions.kt$net.opendasharchive.openarchive.extensions.DrawableExtensions.kt
+ FinalNewline:DrawableUtil.kt$net.opendasharchive.openarchive.util.DrawableUtil.kt
+ FinalNewline:DriveServiceHelper.kt$net.opendasharchive.openarchive.util.DriveServiceHelper.kt
+ FinalNewline:DurationExtensions.kt$net.opendasharchive.openarchive.extensions.DurationExtensions.kt
+ FinalNewline:EditFolderActivity.kt$net.opendasharchive.openarchive.features.settings.EditFolderActivity.kt
+ FinalNewline:Effects.kt$net.opendasharchive.openarchive.core.state.Effects.kt
+ FinalNewline:EmptyableRecyclerView.kt$net.opendasharchive.openarchive.features.main.ui.EmptyableRecyclerView.kt
+ FinalNewline:ExpandableSpaceList.kt$net.opendasharchive.openarchive.features.main.ui.components.ExpandableSpaceList.kt
+ FinalNewline:FeaturesModule.kt$net.opendasharchive.openarchive.core.di.FeaturesModule.kt
+ FinalNewline:FileUploadResult.kt$net.opendasharchive.openarchive.db.FileUploadResult.kt
+ FinalNewline:FileUtils.kt$net.opendasharchive.openarchive.util.FileUtils.kt
+ FinalNewline:FolderAdapter.kt$net.opendasharchive.openarchive.FolderAdapter.kt
+ FinalNewline:FolderDrawerAdapter.kt$net.opendasharchive.openarchive.features.main.adapters.FolderDrawerAdapter.kt
+ FinalNewline:FolderOptionsPopup.kt$net.opendasharchive.openarchive.features.main.ui.components.FolderOptionsPopup.kt
+ FinalNewline:FoldersActivity.kt$net.opendasharchive.openarchive.features.settings.FoldersActivity.kt
+ FinalNewline:FullscreenDimmingOverlay.kt$net.opendasharchive.openarchive.util.FullscreenDimmingOverlay.kt
+ FinalNewline:FullscreenOverlayManager.kt$net.opendasharchive.openarchive.util.FullscreenOverlayManager.kt
+ FinalNewline:GDriveActivity.kt$net.opendasharchive.openarchive.services.gdrive.GDriveActivity.kt
+ FinalNewline:GDriveConduit.kt$net.opendasharchive.openarchive.services.gdrive.GDriveConduit.kt
+ FinalNewline:GDriveFragment.kt$net.opendasharchive.openarchive.services.gdrive.GDriveFragment.kt
+ FinalNewline:GeneralSettingsActivity.kt$net.opendasharchive.openarchive.features.settings.GeneralSettingsActivity.kt
+ FinalNewline:HapticManager.kt$net.opendasharchive.openarchive.features.settings.passcode.HapticManager.kt
+ FinalNewline:HashingStrategy.kt$net.opendasharchive.openarchive.features.settings.passcode.HashingStrategy.kt
+ FinalNewline:Hbks.kt$net.opendasharchive.openarchive.util.Hbks.kt
+ FinalNewline:HomeActivity.kt$net.opendasharchive.openarchive.features.main.HomeActivity.kt
+ FinalNewline:HomeAppBar.kt$net.opendasharchive.openarchive.features.main.ui.components.HomeAppBar.kt
+ FinalNewline:HomeScreen.kt$net.opendasharchive.openarchive.features.main.ui.HomeScreen.kt
+ FinalNewline:HttpLikeException.kt$net.opendasharchive.openarchive.services.snowbird.service.HttpLikeException.kt
+ FinalNewline:ISnowbirdAPI.kt$net.opendasharchive.openarchive.services.snowbird.service.ISnowbirdAPI.kt
+ FinalNewline:InternetArchiveLocalSource.kt$net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveLocalSource.kt
+ FinalNewline:InternetArchiveScreen.kt$net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveScreen.kt
+ FinalNewline:JoinGroupResponse.kt$net.opendasharchive.openarchive.db.JoinGroupResponse.kt
+ FinalNewline:Listener.kt$net.opendasharchive.openarchive.core.state.Listener.kt
+ FinalNewline:MainBottomBar.kt$net.opendasharchive.openarchive.features.main.ui.components.MainBottomBar.kt
+ FinalNewline:MainDrawerContent.kt$net.opendasharchive.openarchive.features.main.ui.components.MainDrawerContent.kt
+ FinalNewline:MainMediaAdapter.kt$net.opendasharchive.openarchive.features.main.adapters.MainMediaAdapter.kt
+ FinalNewline:MainMediaAdapterTest.kt$net.opendasharchive.openarchive.MainMediaAdapterTest.kt
+ FinalNewline:MainMediaScreen.kt$net.opendasharchive.openarchive.features.main.ui.MainMediaScreen.kt
+ FinalNewline:MainViewModel.kt$net.opendasharchive.openarchive.features.main.MainViewModel.kt
+ FinalNewline:MediaAdapter.kt$net.opendasharchive.openarchive.db.MediaAdapter.kt
+ FinalNewline:MediaCacheScreen.kt$net.opendasharchive.openarchive.features.main.ui.MediaCacheScreen.kt
+ FinalNewline:MediaLaunchers.kt$net.opendasharchive.openarchive.features.media.MediaLaunchers.kt
+ FinalNewline:Notifier.kt$net.opendasharchive.openarchive.core.state.Notifier.kt
+ FinalNewline:NumericKeypad.kt$net.opendasharchive.openarchive.features.settings.passcode.components.NumericKeypad.kt
+ FinalNewline:Onboarding23Activity.kt$net.opendasharchive.openarchive.features.onboarding.Onboarding23Activity.kt
+ FinalNewline:Onboarding23FragmentStateAdapter.kt$net.opendasharchive.openarchive.features.onboarding.Onboarding23FragmentStateAdapter.kt
+ FinalNewline:Onboarding23InstructionsActivity.kt$net.opendasharchive.openarchive.features.onboarding.Onboarding23InstructionsActivity.kt
+ FinalNewline:Onboarding23SlideFragment.kt$net.opendasharchive.openarchive.features.onboarding.Onboarding23SlideFragment.kt
+ FinalNewline:PBKDF2HashingStrategy.kt$net.opendasharchive.openarchive.features.settings.passcode.PBKDF2HashingStrategy.kt
+ FinalNewline:PasscodeDots.kt$net.opendasharchive.openarchive.features.settings.passcode.components.PasscodeDots.kt
+ FinalNewline:PasscodeEntryActivity.kt$net.opendasharchive.openarchive.features.settings.passcode.passcode_entry.PasscodeEntryActivity.kt
+ FinalNewline:PasscodeEntryScreen.kt$net.opendasharchive.openarchive.features.settings.passcode.passcode_entry.PasscodeEntryScreen.kt
+ FinalNewline:PasscodeEntryViewModel.kt$net.opendasharchive.openarchive.features.settings.passcode.passcode_entry.PasscodeEntryViewModel.kt
+ FinalNewline:PasscodeManager.kt$net.opendasharchive.openarchive.features.settings.passcode.PasscodeManager.kt
+ FinalNewline:PasscodeModule.kt$net.opendasharchive.openarchive.core.di.PasscodeModule.kt
+ FinalNewline:PasscodeRepository.kt$net.opendasharchive.openarchive.features.settings.passcode.PasscodeRepository.kt
+ FinalNewline:PasscodeSetupActivity.kt$net.opendasharchive.openarchive.features.settings.passcode.passcode_setup.PasscodeSetupActivity.kt
+ FinalNewline:PasscodeSetupScreen.kt$net.opendasharchive.openarchive.features.settings.passcode.passcode_setup.PasscodeSetupScreen.kt
+ FinalNewline:PasscodeSetupViewModel.kt$net.opendasharchive.openarchive.features.settings.passcode.passcode_setup.PasscodeSetupViewModel.kt
+ FinalNewline:Picker.kt$net.opendasharchive.openarchive.features.media.Picker.kt
+ FinalNewline:Preview.kt$net.opendasharchive.openarchive.core.presentation.theme.Preview.kt
+ FinalNewline:PreviewActivity.kt$net.opendasharchive.openarchive.features.media.PreviewActivity.kt
+ FinalNewline:PreviewAdapter.kt$net.opendasharchive.openarchive.features.media.PreviewAdapter.kt
+ FinalNewline:PreviewViewHolder.kt$net.opendasharchive.openarchive.features.media.adapter.PreviewViewHolder.kt
+ FinalNewline:PrimaryButton.kt$net.opendasharchive.openarchive.core.presentation.components.PrimaryButton.kt
+ FinalNewline:ProcessingTracker.kt$net.opendasharchive.openarchive.util.ProcessingTracker.kt
+ FinalNewline:Project.kt$net.opendasharchive.openarchive.db.Project.kt
+ FinalNewline:ProofModeHelper.kt$net.opendasharchive.openarchive.util.ProofModeHelper.kt
+ FinalNewline:ProofModeScreen.kt$net.opendasharchive.openarchive.features.settings.ProofModeScreen.kt
+ FinalNewline:QRScannerActivity.kt$net.opendasharchive.openarchive.features.main.QRScannerActivity.kt
+ FinalNewline:Reducer.kt$net.opendasharchive.openarchive.core.state.Reducer.kt
+ FinalNewline:RequestListener.kt$net.opendasharchive.openarchive.services.internetarchive.RequestListener.kt
+ FinalNewline:RequestNameDTO.kt$net.opendasharchive.openarchive.db.RequestNameDTO.kt
+ FinalNewline:RestEndpointTask.kt$net.opendasharchive.openarchive.features.main.RestEndpointTask.kt
+ FinalNewline:RetrofitAPI.kt$net.opendasharchive.openarchive.services.snowbird.service.RetrofitAPI.kt
+ FinalNewline:RetrofitClient.kt$net.opendasharchive.openarchive.services.snowbird.service.RetrofitClient.kt
+ FinalNewline:RetrofitModule.kt$net.opendasharchive.openarchive.core.di.RetrofitModule.kt
+ FinalNewline:RetryConfig.kt$net.opendasharchive.openarchive.services.snowbird.service.RetryConfig.kt
+ FinalNewline:SaveApp.kt$net.opendasharchive.openarchive.SaveApp.kt
+ FinalNewline:ScryptHashingStrategy.kt$net.opendasharchive.openarchive.features.settings.passcode.ScryptHashingStrategy.kt
+ FinalNewline:SectionViewHolder.kt$net.opendasharchive.openarchive.features.main.SectionViewHolder.kt
+ FinalNewline:SerializableMarker.kt$net.opendasharchive.openarchive.db.SerializableMarker.kt
+ FinalNewline:ServerOptionItem.kt$net.opendasharchive.openarchive.features.spaces.ServerOptionItem.kt
+ FinalNewline:SettingsFragment.kt$net.opendasharchive.openarchive.features.settings.SettingsFragment.kt
+ FinalNewline:SettingsScreen.kt$net.opendasharchive.openarchive.features.settings.SettingsScreen.kt
+ FinalNewline:Shape.kt$net.opendasharchive.openarchive.core.presentation.theme.Shape.kt
+ FinalNewline:SmartFragmentStatePagerAdapter.kt$net.opendasharchive.openarchive.util.SmartFragmentStatePagerAdapter.kt
+ FinalNewline:SnowbirdBridge.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdBridge.kt
+ FinalNewline:SnowbirdConduit.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdConduit.kt
+ FinalNewline:SnowbirdCreateGroupFragment.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdCreateGroupFragment.kt
+ FinalNewline:SnowbirdError.kt$net.opendasharchive.openarchive.db.SnowbirdError.kt
+ FinalNewline:SnowbirdFileListAdapter.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdFileListAdapter.kt
+ FinalNewline:SnowbirdFileListFragment.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdFileListFragment.kt
+ FinalNewline:SnowbirdFileRepository.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdFileRepository.kt
+ FinalNewline:SnowbirdFileViewModel.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdFileViewModel.kt
+ FinalNewline:SnowbirdFragment.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdFragment.kt
+ FinalNewline:SnowbirdGroup.kt$net.opendasharchive.openarchive.db.SnowbirdGroup.kt
+ FinalNewline:SnowbirdGroupListAdapter.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdGroupListAdapter.kt
+ FinalNewline:SnowbirdGroupListFragment.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdGroupListFragment.kt
+ FinalNewline:SnowbirdGroupOverviewFragment.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdGroupOverviewFragment.kt
+ FinalNewline:SnowbirdGroupRepository.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdGroupRepository.kt
+ FinalNewline:SnowbirdGroupViewModel.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdGroupViewModel.kt
+ FinalNewline:SnowbirdJoinGroupFragment.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdJoinGroupFragment.kt
+ FinalNewline:SnowbirdRepo.kt$net.opendasharchive.openarchive.db.SnowbirdRepo.kt
+ FinalNewline:SnowbirdRepoListAdapter.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdRepoListAdapter.kt
+ FinalNewline:SnowbirdRepoListFragment.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdRepoListFragment.kt
+ FinalNewline:SnowbirdRepoRepository.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdRepoRepository.kt
+ FinalNewline:SnowbirdRepoViewModel.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdRepoViewModel.kt
+ FinalNewline:SnowbirdResult.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdResult.kt
+ FinalNewline:SnowbirdService.kt$net.opendasharchive.openarchive.services.snowbird.service.SnowbirdService.kt
+ FinalNewline:SnowbirdServiceStatus.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdServiceStatus.kt
+ FinalNewline:SnowbirdShareFragment.kt$net.opendasharchive.openarchive.services.snowbird.SnowbirdShareFragment.kt
+ FinalNewline:Space.kt$net.opendasharchive.openarchive.db.Space.kt
+ FinalNewline:SpaceAdapter.kt$net.opendasharchive.openarchive.SpaceAdapter.kt
+ FinalNewline:SpaceDrawerAdapter.kt$net.opendasharchive.openarchive.features.main.adapters.SpaceDrawerAdapter.kt
+ FinalNewline:SpaceListFragment.kt$net.opendasharchive.openarchive.features.spaces.SpaceListFragment.kt
+ FinalNewline:SpaceListScreen.kt$net.opendasharchive.openarchive.features.spaces.SpaceListScreen.kt
+ FinalNewline:SpaceSetupFragment.kt$net.opendasharchive.openarchive.features.settings.SpaceSetupFragment.kt
+ FinalNewline:SpaceSetupSuccessFragment.kt$net.opendasharchive.openarchive.features.settings.SpaceSetupSuccessFragment.kt
+ FinalNewline:SpacingItemDecoration.kt$net.opendasharchive.openarchive.util.SpacingItemDecoration.kt
+ FinalNewline:Stateful.kt$net.opendasharchive.openarchive.core.state.Stateful.kt
+ FinalNewline:Store.kt$net.opendasharchive.openarchive.core.state.Store.kt
+ FinalNewline:StringExtensions.kt$net.opendasharchive.openarchive.extensions.StringExtensions.kt
+ FinalNewline:SuspendableExtensions.kt$net.opendasharchive.openarchive.extensions.SuspendableExtensions.kt
+ FinalNewline:SwipeToDeleteCallback.kt$net.opendasharchive.openarchive.upload.SwipeToDeleteCallback.kt
+ FinalNewline:TextView.kt$net.opendasharchive.openarchive.util.extensions.TextView.kt
+ FinalNewline:ThrowableExceptions.kt$net.opendasharchive.openarchive.extensions.ThrowableExceptions.kt
+ FinalNewline:ToolbarConfigurable.kt$net.opendasharchive.openarchive.features.core.ToolbarConfigurable.kt
+ FinalNewline:TorStatusContentProvider.kt$net.opendasharchive.openarchive.provider.TorStatusContentProvider.kt
+ FinalNewline:TorStatusDatabase.kt$net.opendasharchive.openarchive.provider.TorStatusDatabase.kt
+ FinalNewline:TwoLetterDrawable.kt$net.opendasharchive.openarchive.util.TwoLetterDrawable.kt
+ FinalNewline:UiImage.kt$net.opendasharchive.openarchive.features.core.UiImage.kt
+ FinalNewline:UiText.kt$net.opendasharchive.openarchive.features.core.UiText.kt
+ FinalNewline:UnitTests.kt$net.opendasharchive.openarchive.UnitTests.kt
+ FinalNewline:UnixSocketAPI.kt$net.opendasharchive.openarchive.services.snowbird.service.UnixSocketAPI.kt
+ FinalNewline:UnixSocketClient.kt$net.opendasharchive.openarchive.features.main.UnixSocketClient.kt
+ FinalNewline:UnixSocketClientFileExtensions.kt$net.opendasharchive.openarchive.features.main.UnixSocketClientFileExtensions.kt
+ FinalNewline:UnixSocketClientUtilityExtensions.kt$net.opendasharchive.openarchive.features.main.UnixSocketClientUtilityExtensions.kt
+ FinalNewline:UnixSocketModule.kt$net.opendasharchive.openarchive.core.di.UnixSocketModule.kt
+ FinalNewline:UploadManagerActivity.kt$net.opendasharchive.openarchive.upload.UploadManagerActivity.kt
+ FinalNewline:UploadManagerFragment.kt$net.opendasharchive.openarchive.upload.UploadManagerFragment.kt
+ FinalNewline:UploadService.kt$net.opendasharchive.openarchive.upload.UploadService.kt
+ FinalNewline:UriExtensions.kt$net.opendasharchive.openarchive.extensions.UriExtensions.kt
+ FinalNewline:Util.kt$net.opendasharchive.openarchive.services.internetarchive.Util.kt
+ FinalNewline:Utility.kt$net.opendasharchive.openarchive.util.Utility.kt
+ FinalNewline:VideoRequestHandler.kt$net.opendasharchive.openarchive.fragments.VideoRequestHandler.kt
+ FinalNewline:ViewExtension.kt$net.opendasharchive.openarchive.extensions.ViewExtension.kt
+ FinalNewline:WebDAVModel.kt$net.opendasharchive.openarchive.db.WebDAVModel.kt
+ FinalNewline:WebDavActivity.kt$net.opendasharchive.openarchive.services.webdav.WebDavActivity.kt
+ FinalNewline:WebDavConduit.kt$net.opendasharchive.openarchive.services.webdav.WebDavConduit.kt
+ FinalNewline:WebDavFragment.kt$net.opendasharchive.openarchive.services.webdav.WebDavFragment.kt
+ FinalNewline:WebDavSetupLicenseFragment.kt$net.opendasharchive.openarchive.services.webdav.WebDavSetupLicenseFragment.kt
+ ForbiddenComment:FeaturesModule.kt$// TODO: have some registry of feature modules
+ ForbiddenComment:FullscreenDimmingOverlay.kt$FullScreenCreateGroupDimmingOverlay$// TODO: Cancel the offending event
+ ForbiddenComment:FullscreenDimmingOverlay.kt$FullScreenDimmingOverlay$// TODO: Cancel the offending event
+ ForbiddenComment:HomeActivity.kt$HomeActivity$// TODO: Display a dialog or Snackbar explaining why notifications are needed.
+ ForbiddenComment:HomeActivity.kt$HomeActivity$// TODO: Extract path, query parameters, etc.
+ ForbiddenComment:HomeActivity.kt$HomeActivity$// TODO: Launch your preview activity or update the UI as needed.
+ ForbiddenComment:HomeActivity.kt$HomeActivity$// TODO: Refresh projects in MainViewModel
+ ForbiddenComment:HomeActivity.kt$HomeActivity$// TODO: Return your current project from a ViewModel or other state.
+ ForbiddenComment:HomeActivity.kt$HomeActivity$// TODO: Update your UI state, refresh fragment content, etc.
+ ForbiddenComment:HomeActivity.kt$HomeActivity$// TODO: Update your navigation or fragment state to display the selected folder.
+ ForbiddenComment:InternetArchiveLocalSource.kt$InternetArchiveLocalSource$// TODO: just use a memory cache for demo, will need to store in DB
+ ForbiddenComment:InternetArchiveLoginUseCase.kt$InternetArchiveLoginUseCase$// TODO: use local data source for database
+ ForbiddenComment:UploadManagerActivity.kt$UploadManagerActivity.<no name provided>$// // TODO: Record metadata. See iOS implementation.
+ ForbiddenPublicDataClass:ApiError.kt$ApiError$ClientError : ApiError
+ ForbiddenPublicDataClass:ApiError.kt$ApiError$HttpError : ApiError
+ ForbiddenPublicDataClass:ApiError.kt$ApiError$NetworkError : ApiError
+ ForbiddenPublicDataClass:ApiError.kt$ApiError$ServerError : ApiError
+ ForbiddenPublicDataClass:ApiError.kt$ApiError$UnexpectedError : ApiError
+ ForbiddenPublicDataClass:ApiResponse.kt$ApiResponse$ErrorResponse : ApiResponse
+ ForbiddenPublicDataClass:ApiResponse.kt$ApiResponse$ListResponse<T> : ApiResponse
+ ForbiddenPublicDataClass:ApiResponse.kt$ApiResponse$SingleResponse<T> : ApiResponse
+ ForbiddenPublicDataClass:AppConfig.kt$AppConfig
+ ForbiddenPublicDataClass:BackoffStrategy.kt$BackoffStrategy$Exponential : BackoffStrategy
+ ForbiddenPublicDataClass:BackoffStrategy.kt$BackoffStrategy$Linear : BackoffStrategy
+ ForbiddenPublicDataClass:BrowseFoldersViewModel.kt$Folder
+ ForbiddenPublicDataClass:Collection.kt$Collection : SugarRecord
+ ForbiddenPublicDataClass:Colors.kt$ColorTheme
+ ForbiddenPublicDataClass:DialogConfigBuilder.kt$ButtonData
+ ForbiddenPublicDataClass:DialogConfigBuilder.kt$DialogConfig
+ ForbiddenPublicDataClass:Dimensions.kt$DimensionsTheme
+ ForbiddenPublicDataClass:Dimensions.kt$Elevations
+ ForbiddenPublicDataClass:Dimensions.kt$Icons
+ ForbiddenPublicDataClass:Dimensions.kt$Spacing
+ ForbiddenPublicDataClass:FileUploadResult.kt$FileUploadResult : SerializableMarker
+ ForbiddenPublicDataClass:Hbks.kt$Hbks.Availability$Available : Availability
+ ForbiddenPublicDataClass:Hbks.kt$Hbks.Availability$Enroll : Availability
+ ForbiddenPublicDataClass:HomeScreen.kt$HomeScreenAction$AddMediaClicked : HomeScreenAction
+ ForbiddenPublicDataClass:HomeScreen.kt$HomeScreenAction$UpdateSelectedProject : HomeScreenAction
+ ForbiddenPublicDataClass:HomeScreen.kt$HomeScreenState
+ ForbiddenPublicDataClass:InternetArchive.kt$InternetArchive
+ ForbiddenPublicDataClass:InternetArchive.kt$InternetArchive$Auth
+ ForbiddenPublicDataClass:InternetArchive.kt$InternetArchive$MetaData
+ ForbiddenPublicDataClass:InternetArchiveDetailsState.kt$InternetArchiveDetailsState
+ ForbiddenPublicDataClass:InternetArchiveDetailsViewModel.kt$InternetArchiveDetailsViewModel.Action$Load : Action
+ ForbiddenPublicDataClass:InternetArchiveDetailsViewModel.kt$InternetArchiveDetailsViewModel.Action$Loaded : Action
+ ForbiddenPublicDataClass:InternetArchiveLoginRequest.kt$InternetArchiveLoginRequest
+ ForbiddenPublicDataClass:InternetArchiveLoginResponse.kt$InternetArchiveLoginResponse
+ ForbiddenPublicDataClass:InternetArchiveLoginResponse.kt$InternetArchiveLoginResponse$S3
+ ForbiddenPublicDataClass:InternetArchiveLoginResponse.kt$InternetArchiveLoginResponse$Values
+ ForbiddenPublicDataClass:InternetArchiveLoginState.kt$InternetArchiveLoginAction$LoginError : InternetArchiveLoginAction
+ ForbiddenPublicDataClass:InternetArchiveLoginState.kt$InternetArchiveLoginAction$LoginSuccess : InternetArchiveLoginAction
+ ForbiddenPublicDataClass:InternetArchiveLoginState.kt$InternetArchiveLoginAction$UpdatePassword : InternetArchiveLoginAction
+ ForbiddenPublicDataClass:InternetArchiveLoginState.kt$InternetArchiveLoginAction$UpdateUsername : InternetArchiveLoginAction
+ ForbiddenPublicDataClass:InternetArchiveLoginState.kt$InternetArchiveLoginState
+ ForbiddenPublicDataClass:JoinGroupResponse.kt$JoinGroupResponse : SerializableMarker
+ ForbiddenPublicDataClass:MainMediaScreen.kt$CollectionSection
+ ForbiddenPublicDataClass:MainViewModel.kt$MainUiState
+ ForbiddenPublicDataClass:Media.kt$Media : SugarRecord
+ ForbiddenPublicDataClass:MediaCacheScreen.kt$MediaFile
+ ForbiddenPublicDataClass:MediaLaunchers.kt$MediaLaunchers
+ ForbiddenPublicDataClass:PasscodeEntryViewModel.kt$PasscodeEntryScreenAction$OnNumberClick : PasscodeEntryScreenAction
+ ForbiddenPublicDataClass:PasscodeEntryViewModel.kt$PasscodeEntryScreenState
+ ForbiddenPublicDataClass:PasscodeEntryViewModel.kt$PasscodeEntryUiEvent$IncorrectPasscode : PasscodeEntryUiEvent
+ ForbiddenPublicDataClass:PasscodeSetupViewModel.kt$PasscodeSetupUiAction$OnNumberClick : PasscodeSetupUiAction
+ ForbiddenPublicDataClass:PasscodeSetupViewModel.kt$PasscodeSetupUiState
+ ForbiddenPublicDataClass:Project.kt$Project : SugarRecord
+ ForbiddenPublicDataClass:RequestNameDTO.kt$MembershipRequest : SerializableMarker
+ ForbiddenPublicDataClass:RequestNameDTO.kt$RequestName : SerializableMarker
+ ForbiddenPublicDataClass:RetryConfig.kt$RetryConfig
+ ForbiddenPublicDataClass:SectionViewHolder.kt$SectionViewHolder
+ ForbiddenPublicDataClass:SnowbirdError.kt$SnowbirdError$GeneralError : SnowbirdError
+ ForbiddenPublicDataClass:SnowbirdError.kt$SnowbirdError$NetworkError : SnowbirdError
+ ForbiddenPublicDataClass:SnowbirdFileItem.kt$SnowbirdFileItem : SugarRecordSerializableMarker
+ ForbiddenPublicDataClass:SnowbirdFileItem.kt$SnowbirdFileList : SerializableMarker
+ ForbiddenPublicDataClass:SnowbirdFileViewModel.kt$SnowbirdFileViewModel.State$DownloadSuccess : State
+ ForbiddenPublicDataClass:SnowbirdFileViewModel.kt$SnowbirdFileViewModel.State$Error : State
+ ForbiddenPublicDataClass:SnowbirdFileViewModel.kt$SnowbirdFileViewModel.State$FetchSuccess : State
+ ForbiddenPublicDataClass:SnowbirdFileViewModel.kt$SnowbirdFileViewModel.State$UploadSuccess : State
+ ForbiddenPublicDataClass:SnowbirdGroup.kt$SnowbirdGroup : SugarRecordSerializableMarker
+ ForbiddenPublicDataClass:SnowbirdGroup.kt$SnowbirdGroupList : SerializableMarker
+ ForbiddenPublicDataClass:SnowbirdGroupViewModel.kt$SnowbirdGroupViewModel.GroupState$Error : GroupState
+ ForbiddenPublicDataClass:SnowbirdGroupViewModel.kt$SnowbirdGroupViewModel.GroupState$JoinGroupSuccess : GroupState
+ ForbiddenPublicDataClass:SnowbirdGroupViewModel.kt$SnowbirdGroupViewModel.GroupState$MultiGroupSuccess : GroupState
+ ForbiddenPublicDataClass:SnowbirdGroupViewModel.kt$SnowbirdGroupViewModel.GroupState$SingleGroupSuccess : GroupState
+ ForbiddenPublicDataClass:SnowbirdRepo.kt$SnowbirdRepo : SugarRecordSerializableMarker
+ ForbiddenPublicDataClass:SnowbirdRepo.kt$SnowbirdRepoList : SerializableMarker
+ ForbiddenPublicDataClass:SnowbirdRepoViewModel.kt$SnowbirdRepoViewModel.RepoState$Error : RepoState
+ ForbiddenPublicDataClass:SnowbirdRepoViewModel.kt$SnowbirdRepoViewModel.RepoState$MultiRepoSuccess : RepoState
+ ForbiddenPublicDataClass:SnowbirdRepoViewModel.kt$SnowbirdRepoViewModel.RepoState$RepoFetchSuccess : RepoState
+ ForbiddenPublicDataClass:SnowbirdRepoViewModel.kt$SnowbirdRepoViewModel.RepoState$SingleRepoSuccess : RepoState
+ ForbiddenPublicDataClass:SnowbirdResult.kt$SnowbirdResult$Error : SnowbirdResult
+ ForbiddenPublicDataClass:SnowbirdResult.kt$SnowbirdResult$Success<out T> : SnowbirdResult
+ ForbiddenPublicDataClass:SnowbirdService.kt$ServiceStatus$Failed : ServiceStatus
+ ForbiddenPublicDataClass:SnowbirdServiceStatus.kt$SnowbirdServiceStatus$Error : SnowbirdServiceStatus
+ ForbiddenPublicDataClass:Space.kt$Space : SugarRecord
+ ForbiddenPublicDataClass:SpaceDrawerAdapter.kt$SpaceDrawerAdapter.SpaceItem$SpaceItemData : SpaceItem
+ ForbiddenPublicDataClass:SuspendableExtensions.kt$RetryAttempt$Failure : RetryAttempt
+ ForbiddenPublicDataClass:SuspendableExtensions.kt$RetryAttempt$Retry : RetryAttempt
+ ForbiddenPublicDataClass:SuspendableExtensions.kt$RetryAttempt$Success<T> : RetryAttemptRetryResult
+ ForbiddenPublicDataClass:UiImage.kt$UiImage$DrawableResource : UiImage
+ ForbiddenPublicDataClass:UiImage.kt$UiImage$DynamicVector : UiImage
+ ForbiddenPublicDataClass:UiText.kt$UiText$DynamicString : UiText
+ ForbiddenPublicDataClass:UiText.kt$UiText$StringResource : UiText
+ ForbiddenPublicDataClass:WebDAVModel.kt$BackendCapabilities
+ ForbiddenPublicDataClass:WebDAVModel.kt$Data
+ ForbiddenPublicDataClass:WebDAVModel.kt$Meta
+ ForbiddenPublicDataClass:WebDAVModel.kt$Ocs
+ ForbiddenPublicDataClass:WebDAVModel.kt$Quota
+ ForbiddenPublicDataClass:WebDAVModel.kt$WebDAVModel
+ FunctionNaming:Accordion.kt$@Composable fun Accordion( modifier: Modifier = Modifier, headerModifier: Modifier = Modifier, state: AccordionState = rememberAccordionState(), animate: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, headerContent: @Composable () -> Unit, bodyContent: @Composable () -> Unit, )
+ FunctionNaming:AddFolderScreen.kt$@Composable fun AddFolderScreen()
+ FunctionNaming:AddFolderScreen.kt$@Composable fun AddFolderScreenContent( onCreateFolder: () -> Unit, onBrowseFolders: () -> Unit )
+ FunctionNaming:AddFolderScreen.kt$@Composable fun FolderOption(iconRes: Int, text: String, onClick: () -> Unit)
+ FunctionNaming:AddFolderScreen.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun AddFolderScreenPreview()
+ FunctionNaming:BaseButton.kt$@Composable fun BaseButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, backgroundColor: Color = MaterialTheme.colorScheme.primary, textColor: Color = MaterialTheme.colorScheme.onPrimary, cornerRadius: Dp = 12.dp, )
+ FunctionNaming:BaseButton.kt$@Composable fun BaseDestructiveButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, borderColor: Color = MaterialTheme.colorScheme.error, textColor: Color = MaterialTheme.colorScheme.error, cornerRadius: Dp = 12.dp, )
+ FunctionNaming:BaseButton.kt$@Composable fun BaseNeutralButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, textColor: Color = MaterialTheme.colorScheme.onPrimary, )
+ FunctionNaming:BaseButton.kt$@Composable fun ButtonText( text: String, modifier: Modifier = Modifier, fontSize: TextUnit = 16.sp, fontWeight: FontWeight = FontWeight.SemiBold, color: Color = MaterialTheme.colorScheme.onPrimary )
+ FunctionNaming:BaseButton.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun CustomButtonPreview()
+ FunctionNaming:BaseButton.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun CustomDestructiveButtonPreview()
+ FunctionNaming:BaseButton.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun CustomNeutralButtonPreview()
+ FunctionNaming:BaseDialog.kt$@Composable fun BaseDialog( onDismiss: () -> Unit, icon: UiImage? = null, iconColor: Color? = null, title: String, message: String, hasCheckbox: Boolean = false, onCheckBoxStateChanged: (Boolean) -> Unit = {}, checkBoxHint: String = "Do not show me this again", positiveButton: ButtonData? = null, neutralButton: ButtonData? = null, destructiveButton: ButtonData? = null, backgroundColor: Color = MaterialTheme.colorScheme.surface )
+ FunctionNaming:BaseDialog.kt$@Composable fun BaseDialogMessage( text: String, modifier: Modifier = Modifier )
+ FunctionNaming:BaseDialog.kt$@Composable fun BaseDialogTitle( text: String, modifier: Modifier = Modifier )
+ FunctionNaming:BaseDialog.kt$@Composable fun DialogHost(dialogStateManager: DialogStateManager)
+ FunctionNaming:BaseDialog.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun BaseDialogPreview()
+ FunctionNaming:BaseDialog.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun ErrorDialogPreview()
+ FunctionNaming:BaseDialog.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun WarningDialogPreview()
+ FunctionNaming:BrowseFolderScreen.kt$@Composable fun BrowseFolderItem( folder: Folder, onClick: () -> Unit )
+ FunctionNaming:BrowseFolderScreen.kt$@Composable fun BrowseFolderScreen( viewModel: BrowseFoldersViewModel = koinViewModel() )
+ FunctionNaming:BrowseFolderScreen.kt$@Composable fun BrowseFolderScreenContent( folders: List<Folder> )
+ FunctionNaming:BrowseFolderScreen.kt$@Preview @Composable private fun BrowseFolderScreenPreview()
+ FunctionNaming:DefaultScaffold.kt$@Composable fun DefaultScaffold( modifier: Modifier = Modifier, topAppBar: (@Composable () -> Unit)? = null, content: @Composable () -> Unit )
+ FunctionNaming:ExpandableSpaceList.kt$@Composable fun DrawerSpaceListItem( space: Space, )
+ FunctionNaming:ExpandableSpaceList.kt$@Composable fun ExpandableSpaceList( serverAccordionState: AccordionState, selectedSpace: Space? = null, spaceList: List<Space> )
+ FunctionNaming:ExpandableSpaceList.kt$@Composable fun SpaceIcon( type: Space.Type, modifier: Modifier = Modifier, tint: Color? = null )
+ FunctionNaming:ExpandableSpaceList.kt$@Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun ExpandableSpaceListPreview()
+ FunctionNaming:FolderOptionsPopup.kt$@Composable fun FolderOptionsPopup( expanded: Boolean = false, onDismissRequest: () -> Unit, onRenameFolder: () -> Unit, onSelectMedia: () -> Unit, onRemoveFolder: () -> Unit )
+ FunctionNaming:FolderOptionsPopup.kt$@Preview @Composable private fun FolderOptionsPopupPreview()
+ FunctionNaming:HomeAppBar.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeAppBar( openDrawer: () -> Unit, onExit: () -> Unit )
+ FunctionNaming:HomeScreen.kt$@Composable fun HomeScreen( viewModel: HomeViewModel = koinViewModel(), onExit: () -> Unit, onNewFolder: () -> Unit, onFolderSelected: (Long) -> Unit, onAddMedia: (AddMediaType) -> Unit, onNavigateToCache: () -> Unit )
+ FunctionNaming:HomeScreen.kt$@Composable fun HomeScreenContent( onExit: () -> Unit, state: HomeScreenState, onAction: (HomeScreenAction) -> Unit, onNavigateToCache: () -> Unit = {} )
+ FunctionNaming:HomeScreen.kt$@Composable fun SaveNavGraph( context: Context, viewModel: HomeViewModel = koinViewModel(), onExit: () -> Unit, onNewFolder: () -> Unit, onFolderSelected: (Long) -> Unit, onAddMedia: (AddMediaType) -> Unit )
+ FunctionNaming:HomeScreen.kt$@Preview @Composable private fun MainContentPreview()
+ FunctionNaming:InternetArchiveDetailsScreen.kt$@Composable @Preview(showBackground = true) @Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) private fun InternetArchiveScreenPreview()
+ FunctionNaming:InternetArchiveDetailsScreen.kt$@Composable fun InternetArchiveDetailsScreen(space: Space, onResult: (IAResult) -> Unit)
+ FunctionNaming:InternetArchiveDetailsScreen.kt$@Composable private fun InternetArchiveDetailsContent( state: InternetArchiveDetailsState, dispatch: Dispatch<Action>, dialogManager: DialogStateManager = koinViewModel() )
+ FunctionNaming:InternetArchiveHeader.kt$@Composable @Preview(showBackground = true) @Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) private fun InternetArchiveHeaderPreview()
+ FunctionNaming:InternetArchiveHeader.kt$@Composable fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 18.sp)
+ FunctionNaming:InternetArchiveLoginScreen.kt$@Composable @Preview @Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) private fun InternetArchiveLoginPreview()
+ FunctionNaming:InternetArchiveLoginScreen.kt$@Composable fun CustomSecureField( modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: String, placeholder: String, isError: Boolean = false, isLoading: Boolean = false, keyboardType: KeyboardType, imeAction: ImeAction, )
+ FunctionNaming:InternetArchiveLoginScreen.kt$@Composable fun CustomTextField( modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: String, enabled: Boolean = true, placeholder: String? = null, isError: Boolean = false, isLoading: Boolean = false, keyboardType: KeyboardType = KeyboardType.Text, imeAction: ImeAction = ImeAction.Next, )
+ FunctionNaming:InternetArchiveLoginScreen.kt$@Composable fun InternetArchiveLoginScreen(space: Space, onResult: (IAResult) -> Unit)
+ FunctionNaming:InternetArchiveLoginScreen.kt$@Composable private fun InternetArchiveLoginContent( state: InternetArchiveLoginState, dispatch: Dispatch<Action> )
+ FunctionNaming:InternetArchiveLoginScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun ComposeAppBar( title: String = "Save App", onNavigationAction: () -> Unit = {} )
+ FunctionNaming:InternetArchiveScreen.kt$@Composable fun InternetArchiveScreen(space: Space, isNewSpace: Boolean, onFinish: (IAResult) -> Unit)
+ FunctionNaming:MainBottomBar.kt$@Composable fun MainBottomBar( isSettings: Boolean, onMyMediaClick: () -> Unit, onSettingsClick: () -> Unit, onAddMediaClick: () -> Unit )
+ FunctionNaming:MainBottomBar.kt$@Composable fun RowScope.BottomNavMenuItem( selectedIcon: ImageVector, unSelectedIcon: ImageVector, isSelected: Boolean, text: String, onClick: () -> Unit )
+ FunctionNaming:MainDrawerContent.kt$@Composable fun MainDrawerContent( selectedSpace: Space? = null, spaceList: List<Space> = emptyList() )
+ FunctionNaming:MainDrawerContent.kt$@Composable fun MainDrawerFolderListItem( project: Project, isSelected: Boolean = false, onSelected: () -> Unit )
+ FunctionNaming:MainDrawerContent.kt$@Preview @Composable private fun MainDrawerContentPreview()
+ FunctionNaming:MainMediaScreen.kt$@Composable fun CollectionHeaderView(section: CollectionSection)
+ FunctionNaming:MainMediaScreen.kt$@Composable fun CollectionSectionView( section: CollectionSection, onMediaClick: (Media) -> Unit, onMediaLongPress: (Media) -> Unit )
+ FunctionNaming:MainMediaScreen.kt$@Composable fun ErrorIndicator()
+ FunctionNaming:MainMediaScreen.kt$@Composable fun MainMediaScreen( projectId: Long, )
+ FunctionNaming:MainMediaScreen.kt$@Composable fun MediaItemView( media: Media, isSelected: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier )
+ FunctionNaming:MainMediaScreen.kt$@Composable fun UploadProgress(progress: Int)
+ FunctionNaming:MainMediaScreen.kt$@Composable fun WelcomeMessage()
+ FunctionNaming:MediaCacheScreen.kt$@Composable fun CacheFileItem(file: MediaFile)
+ FunctionNaming:MediaCacheScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaCacheScreen(context: Context, onNavigateBack: () -> Unit)
+ FunctionNaming:NumericKeypad.kt$@Composable fun NumericKeypad( isEnabled: Boolean = true, onNumberClick: (String) -> Unit, onDeleteClick: () -> Unit, onSubmitClick: () -> Unit )
+ FunctionNaming:NumericKeypad.kt$@Composable private fun NumberButton( label: String, enabled: Boolean = true, onClick: () -> Unit, hapticManager: HapticManager = koinInject() )
+ FunctionNaming:NumericKeypad.kt$@Preview @Composable private fun NumericKeypadPreview()
+ FunctionNaming:PasscodeDots.kt$@Composable fun PasscodeDots( passcodeLength: Int, currentPasscodeLength: Int, shouldShake: Boolean = false )
+ FunctionNaming:PasscodeDots.kt$@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview @Composable private fun PasswordDotsPreview()
+ FunctionNaming:PasscodeEntryScreen.kt$@Composable fun PasscodeEntryScreen( onPasscodeSuccess: () -> Unit, onExit: () -> Unit, viewModel: PasscodeEntryViewModel = koinViewModel(), hapticManager: HapticManager = koinInject() )
+ FunctionNaming:PasscodeEntryScreen.kt$@Composable fun PasscodeEntryScreenContent( state: PasscodeEntryScreenState, onAction: (PasscodeEntryScreenAction) -> Unit, onExit: () -> Unit, )
+ FunctionNaming:PasscodeEntryScreen.kt$@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview @Composable private fun PasscodeEntryScreenPreview()
+ FunctionNaming:PasscodeSetupScreen.kt$@Composable fun PasscodeSetupScreen( onPasscodeSet: () -> Unit, onCancel: () -> Unit, viewModel: PasscodeSetupViewModel = koinViewModel(), hapticManager: HapticManager = koinInject() )
+ FunctionNaming:PasscodeSetupScreen.kt$@Composable private fun PasscodeSetupScreenContent( state: PasscodeSetupUiState, onAction: (PasscodeSetupUiAction) -> Unit )
+ FunctionNaming:PasscodeSetupScreen.kt$@Preview(uiMode = UI_MODE_NIGHT_YES) @Preview @Composable private fun PasscodeSetupScreenPreview()
+ FunctionNaming:Preview.kt$@Composable fun DefaultBoxPreview( content: @Composable () -> Unit )
+ FunctionNaming:Preview.kt$@Composable fun DefaultEmptyScaffoldPreview( content: @Composable () -> Unit )
+ FunctionNaming:Preview.kt$@Composable fun DefaultScaffoldPreview( content: @Composable () -> Unit )
+ FunctionNaming:PrimaryButton.kt$@Composable fun PrimaryButton( modifier: Modifier = Modifier, icon: ImageVector? = null, text: String, onClick: () -> Unit )
+ FunctionNaming:PrimaryButton.kt$@Preview @Composable private fun PrimaryButtonPreview()
+ FunctionNaming:ProofModeScreen.kt$@Composable fun ProofModeScreen( onNavigateBack: () -> Unit )
+ FunctionNaming:ProofModeScreen.kt$@Composable fun ProofModeScreenContent()
+ FunctionNaming:ProofModeScreen.kt$@Preview @Composable private fun ProofModeScreenPreview()
+ FunctionNaming:ServerOptionItem.kt$@Composable fun ServerOptionItem( @DrawableRes iconRes: Int, title: String, subtitle: String, onClick: () -> Unit )
+ FunctionNaming:ServerOptionItem.kt$@Preview @Composable private fun ServerOptionItemPreview()
+ FunctionNaming:SettingsScreen.kt$@Composable fun SettingsScreen( onNavigateToCache: () -> Unit = {} )
+ FunctionNaming:SettingsScreen.kt$@Preview @Composable private fun SettingsScreenPreview()
+ FunctionNaming:SpaceListScreen.kt$@Composable fun SpaceListItem( space: Space, onClick: () -> Unit )
+ FunctionNaming:SpaceListScreen.kt$@Composable fun SpaceListScreen( onSpaceClicked: (Space) -> Unit, )
+ FunctionNaming:SpaceListScreen.kt$@Composable fun SpaceListScreenContent( onSpaceClicked: (Space) -> Unit, spaceList: List<Space> = emptyList() )
+ FunctionNaming:SpaceListScreen.kt$@Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun SpaceListScreenPreview()
+ FunctionNaming:SpaceSetupScreen.kt$@Composable fun SpaceSetupScreen( onWebDavClick: () -> Unit, isInternetArchiveAllowed: Boolean, onInternetArchiveClick: () -> Unit, isDwebEnabled: Boolean, onDwebClicked: () -> Unit )
+ FunctionNaming:SpaceSetupScreen.kt$@Preview @Composable private fun SpaceSetupScreenPreview()
+ FunctionNaming:Theme.kt$@Composable fun SaveAppTheme( content: @Composable () -> Unit )
+ FunctionNaming:TwoLetterDrawable.kt$TwoLetterDrawable.Companion$fun ReadOnly(context: Context)
+ FunctionNaming:TwoLetterDrawable.kt$TwoLetterDrawable.Companion$fun ReadWrite(context: Context)
+ FunctionOnlyReturningConstant:HomeActivity.kt$HomeActivity$private fun getCurrentProject(): Project?
+ FunctionStartOfBodySpacing:InternetArchiveLocalSource.kt$InternetArchiveLocalSource$fun set(value: InternetArchive)
+ ImportOrdering:ApplicationExtensions.kt$import android.app.Application import androidx.activity.ComponentActivity import androidx.fragment.app.Fragment import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import org.koin.android.ext.android.getKoin import org.koin.core.parameter.parametersOf import org.koin.androidx.viewmodel.ext.android.viewModel
+ ImportOrdering:FeaturesModule.kt$import android.app.Application import android.content.Context import net.opendasharchive.openarchive.features.internetarchive.internetArchiveModule import net.opendasharchive.openarchive.features.settings.passcode.AppConfig import net.opendasharchive.openarchive.features.settings.passcode.HapticManager import net.opendasharchive.openarchive.features.settings.passcode.HashingStrategy import net.opendasharchive.openarchive.features.settings.passcode.PBKDF2HashingStrategy import net.opendasharchive.openarchive.features.settings.passcode.passcode_entry.PasscodeEntryViewModel import net.opendasharchive.openarchive.features.settings.passcode.PasscodeRepository import net.opendasharchive.openarchive.features.settings.passcode.passcode_setup.PasscodeSetupViewModel import net.opendasharchive.openarchive.services.snowbird.ISnowbirdFileRepository import net.opendasharchive.openarchive.services.snowbird.ISnowbirdGroupRepository import net.opendasharchive.openarchive.services.snowbird.ISnowbirdRepoRepository import net.opendasharchive.openarchive.services.snowbird.SnowbirdFileRepository import net.opendasharchive.openarchive.services.snowbird.SnowbirdFileViewModel import net.opendasharchive.openarchive.services.snowbird.SnowbirdGroupRepository import net.opendasharchive.openarchive.services.snowbird.SnowbirdGroupViewModel import net.opendasharchive.openarchive.services.snowbird.SnowbirdRepoRepository import net.opendasharchive.openarchive.services.snowbird.SnowbirdRepoViewModel import org.koin.core.module.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module
+ ImportOrdering:InternetArchiveDetailsScreen.kt$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.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton 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.graphics.Color 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.presentation.theme.SaveAppTheme 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 net.opendasharchive.openarchive.features.internetarchive.presentation.login.CustomTextField import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview import net.opendasharchive.openarchive.features.core.UiImage import net.opendasharchive.openarchive.features.core.UiText import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager import net.opendasharchive.openarchive.features.core.dialog.showDialog import net.opendasharchive.openarchive.features.core.dialog.showSuccessDialog import net.opendasharchive.openarchive.features.core.dialog.showWarningDialog import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf
+ ImportOrdering:InternetArchiveFragment.kt$import android.os.Bundle 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.setFragmentResult import androidx.navigation.fragment.findNavController import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult import net.opendasharchive.openarchive.features.internetarchive.presentation.components.bundleWithNewSpace import net.opendasharchive.openarchive.features.internetarchive.presentation.components.bundleWithSpaceId import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace import net.opendasharchive.openarchive.features.core.BaseFragment import net.opendasharchive.openarchive.features.core.ToolbarConfigurable
+ ImportOrdering:SnowbirdFragment.kt$import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts import androidx.core.os.bundleOf import androidx.fragment.app.setFragmentResult import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import com.google.zxing.integration.android.IntentIntegrator import kotlinx.coroutines.launch import net.opendasharchive.openarchive.databinding.FragmentSnowbirdBinding import net.opendasharchive.openarchive.db.SnowbirdGroup import net.opendasharchive.openarchive.extensions.getQueryParameter import net.opendasharchive.openarchive.features.main.QRScannerActivity import net.opendasharchive.openarchive.features.core.BaseFragment import net.opendasharchive.openarchive.util.Utility import timber.log.Timber
+ ImportOrdering:StatefulViewModel.kt$import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import net.opendasharchive.openarchive.core.state.StateDispatcher import net.opendasharchive.openarchive.core.state.StoreObserver import net.opendasharchive.openarchive.core.state.Stateful import net.opendasharchive.openarchive.core.state.Store
+ ImportOrdering:VideoRequestHandler.kt$import android.content.Context import android.graphics.Bitmap import com.squareup.picasso.Picasso import android.media.MediaMetadataRetriever import android.net.Uri import com.squareup.picasso.Request import com.squareup.picasso.RequestHandler import java.io.IOException import java.lang.Exception import androidx.core.net.toUri
+ Indentation:Accordion.kt$
+ Indentation:BaseButton.kt$
+ Indentation:BaseDialog.kt$
+ Indentation:BrowseFoldersFragment.kt$BrowseFoldersFragment$
+ Indentation:CleanInsightsManager.kt$CleanInsightsManager.<no name provided>$
+ Indentation:EditFolderActivity.kt$EditFolderActivity$
+ Indentation:FileUtils.kt$FileUtils$
+ Indentation:GDriveActivity.kt$GDriveActivity$
+ Indentation:GDriveFragment.kt$GDriveFragment$
+ Indentation:Hbks.kt$Hbks.<no name provided>$
+ Indentation:HomeScreen.kt$
+ Indentation:InternetArchiveActivity.kt$InternetArchiveActivity$
+ Indentation:InternetArchiveDetailsViewModel.kt$InternetArchiveDetailsViewModel$
+ Indentation:InternetArchiveHeader.kt$
+ Indentation:MainMediaAdapter.kt$MediaDiffCallback$
+ Indentation:Media.kt$Media$
+ Indentation:MediaAdapter.kt$MediaAdapter$
+ Indentation:MediaAdapter.kt$MediaDiffCallback$
+ Indentation:MediaCacheScreen.kt$
+ Indentation:Onboarding23InstructionsActivity.kt$Onboarding23InstructionsActivity.<no name provided>$
+ Indentation:PasscodeManager.kt$PasscodeManager$
+ Indentation:PasscodeSetupActivity.kt$PasscodeSetupActivity$
+ Indentation:Picker.kt$Picker$
+ Indentation:PreviewAdapter.kt$PreviewAdapter.Companion.<no name provided>$
+ Indentation:Project.kt$Project$
+ Indentation:ProofModeScreen.kt$
+ Indentation:RequestBodyUtil.kt$RequestBodyUtil$
+ Indentation:RequestBodyUtil.kt$RequestBodyUtil.<no name provided>$
+ Indentation:SnowbirdFileListFragment.kt$SnowbirdFileListFragment$
+ Indentation:SnowbirdFileListFragment.kt$SnowbirdFileListFragment.<no name provided>$
+ Indentation:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment.<no name provided>$
+ Indentation:SnowbirdJoinGroupFragment.kt$SnowbirdJoinGroupFragment$
+ Indentation:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment.<no name provided>$
+ Indentation:SpaceAdapter.kt$SpaceAdapter$
+ Indentation:SpaceListScreen.kt$
+ Indentation:TextView.kt$
+ Indentation:UnixSocketAPI.kt$UnixSocketAPI$
+ Indentation:UriExtensions.kt$
+ Indentation:ValidateLoginCredentialsUseCase.kt$ValidateLoginCredentialsUseCase$
+ Indentation:WebDavFragment.kt$WebDavFragment$
+ LambdaParameterEventTrailing:PrimaryButton.kt$onClick
+ LambdaParameterInRestartableEffect:HomeScreen.kt$onAction
+ LambdaParameterInRestartableEffect:InternetArchiveDetailsScreen.kt$onResult
+ LambdaParameterInRestartableEffect:InternetArchiveLoginScreen.kt$onResult
+ LambdaParameterInRestartableEffect:PasscodeEntryScreen.kt$onExit
+ LambdaParameterInRestartableEffect:PasscodeEntryScreen.kt$onPasscodeSuccess
+ LambdaParameterInRestartableEffect:PasscodeSetupScreen.kt$onCancel
+ LambdaParameterInRestartableEffect:PasscodeSetupScreen.kt$onPasscodeSet
+ LibraryEntitiesShouldNotBePublic:Accordion.kt$@Composable fun Accordion( modifier: Modifier = Modifier, headerModifier: Modifier = Modifier, state: AccordionState = rememberAccordionState(), animate: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, headerContent: @Composable () -> Unit, bodyContent: @Composable () -> Unit, )
+ LibraryEntitiesShouldNotBePublic:Accordion.kt$@Composable fun rememberAccordionGroupState( count: Int, allowMultipleOpen: Boolean = false, ): AccordionGroupState
+ LibraryEntitiesShouldNotBePublic:Accordion.kt$@Composable fun rememberAccordionState( expanded: Boolean = false, enabled: Boolean = true, clickable: Boolean = true, onExpandedChange: ((Boolean) -> Unit)? = null, )
+ LibraryEntitiesShouldNotBePublic:Accordion.kt$AccordionGroupState
+ LibraryEntitiesShouldNotBePublic:Accordion.kt$AccordionState
+ LibraryEntitiesShouldNotBePublic:ActivityExtension.kt$fun Activity.onBackButtonPressed(callback: () -> Boolean)
+ LibraryEntitiesShouldNotBePublic:AddFolderActivity.kt$AddFolderActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:AddFolderScreen.kt$@Composable fun AddFolderScreen()
+ LibraryEntitiesShouldNotBePublic:AddFolderScreen.kt$@Composable fun AddFolderScreenContent( onCreateFolder: () -> Unit, onBrowseFolders: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:AddFolderScreen.kt$@Composable fun FolderOption(iconRes: Int, text: String, onClick: () -> Unit)
+ LibraryEntitiesShouldNotBePublic:AddMediaDialogFragment.kt$AddMediaDialogFragment : DialogFragment
+ LibraryEntitiesShouldNotBePublic:AddMediaType.kt$AddMediaType
+ LibraryEntitiesShouldNotBePublic:AlertHelper.kt$AlertHelper
+ LibraryEntitiesShouldNotBePublic:ApiError.kt$ApiError : SerializableMarker
+ LibraryEntitiesShouldNotBePublic:ApiResponse.kt$ApiResponse<out T>
+ LibraryEntitiesShouldNotBePublic:AppConfig.kt$AppConfig
+ LibraryEntitiesShouldNotBePublic:ApplicationExtensions.kt$inline fun <reified T : AndroidViewModel> ComponentActivity.androidViewModel(): Lazy<T>
+ LibraryEntitiesShouldNotBePublic:ApplicationExtensions.kt$inline fun <reified T : AndroidViewModel> Fragment.androidViewModel(): Lazy<T>
+ LibraryEntitiesShouldNotBePublic:ApplicationExtensions.kt$inline fun <reified T : ViewModel> Application.getViewModel(vararg parameters: Any): T
+ LibraryEntitiesShouldNotBePublic:BackoffStrategy.kt$BackoffStrategy
+ LibraryEntitiesShouldNotBePublic:BadgeDrawable.kt$BadgeDrawable : Drawable
+ LibraryEntitiesShouldNotBePublic:BaseActivity.kt$BaseActivity : AppCompatActivity
+ LibraryEntitiesShouldNotBePublic:BaseButton.kt$@Composable fun BaseButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, backgroundColor: Color = MaterialTheme.colorScheme.primary, textColor: Color = MaterialTheme.colorScheme.onPrimary, cornerRadius: Dp = 12.dp, )
+ LibraryEntitiesShouldNotBePublic:BaseButton.kt$@Composable fun BaseDestructiveButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, borderColor: Color = MaterialTheme.colorScheme.error, textColor: Color = MaterialTheme.colorScheme.error, cornerRadius: Dp = 12.dp, )
+ LibraryEntitiesShouldNotBePublic:BaseButton.kt$@Composable fun BaseNeutralButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, textColor: Color = MaterialTheme.colorScheme.onPrimary, )
+ LibraryEntitiesShouldNotBePublic:BaseButton.kt$@Composable fun ButtonText( text: String, modifier: Modifier = Modifier, fontSize: TextUnit = 16.sp, fontWeight: FontWeight = FontWeight.SemiBold, color: Color = MaterialTheme.colorScheme.onPrimary )
+ LibraryEntitiesShouldNotBePublic:BaseComposeActivity.kt$BaseComposeActivity : AppCompatActivity
+ LibraryEntitiesShouldNotBePublic:BaseDialog.kt$@Composable fun BaseDialog( onDismiss: () -> Unit, icon: UiImage? = null, iconColor: Color? = null, title: String, message: String, hasCheckbox: Boolean = false, onCheckBoxStateChanged: (Boolean) -> Unit = {}, checkBoxHint: String = "Do not show me this again", positiveButton: ButtonData? = null, neutralButton: ButtonData? = null, destructiveButton: ButtonData? = null, backgroundColor: Color = MaterialTheme.colorScheme.surface )
+ LibraryEntitiesShouldNotBePublic:BaseDialog.kt$@Composable fun BaseDialogMessage( text: String, modifier: Modifier = Modifier )
+ LibraryEntitiesShouldNotBePublic:BaseDialog.kt$@Composable fun BaseDialogTitle( text: String, modifier: Modifier = Modifier )
+ LibraryEntitiesShouldNotBePublic:BaseDialog.kt$@Composable fun DialogHost(dialogStateManager: DialogStateManager)
+ LibraryEntitiesShouldNotBePublic:BaseDialog.kt$DialogStateManager : ViewModel
+ LibraryEntitiesShouldNotBePublic:BaseFragment.kt$BaseFragment : FragmentToolbarConfigurable
+ LibraryEntitiesShouldNotBePublic:BaseSnowbirdFragment.kt$BaseSnowbirdFragment : Fragment
+ LibraryEntitiesShouldNotBePublic:BaseViewModel.kt$BaseViewModel : AndroidViewModel
+ LibraryEntitiesShouldNotBePublic:BasicAuthInterceptor.kt$BasicAuthInterceptor : Interceptor
+ LibraryEntitiesShouldNotBePublic:BiometricAuthenticator.kt$BiometricAuthenticator
+ LibraryEntitiesShouldNotBePublic:BottomSheetExtensions.kt$fun Fragment.showBottomSheetDialog( @LayoutRes layout: Int, @IdRes textViewToSet: Int? = null, textToSet: String? = null, fullScreen: Boolean = true, expand: Boolean = true )
+ LibraryEntitiesShouldNotBePublic:BroadcastManager.kt$BroadcastManager$Action
+ LibraryEntitiesShouldNotBePublic:BrowseFolderScreen.kt$@Composable fun BrowseFolderItem( folder: Folder, onClick: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:BrowseFolderScreen.kt$@Composable fun BrowseFolderScreen( viewModel: BrowseFoldersViewModel = koinViewModel() )
+ LibraryEntitiesShouldNotBePublic:BrowseFolderScreen.kt$@Composable fun BrowseFolderScreenContent( folders: List<Folder> )
+ LibraryEntitiesShouldNotBePublic:BrowseFoldersAdapter.kt$BrowseFoldersAdapter : Adapter
+ LibraryEntitiesShouldNotBePublic:BrowseFoldersFragment.kt$BrowseFoldersFragment : BaseFragmentMenuProvider
+ LibraryEntitiesShouldNotBePublic:BrowseFoldersViewModel.kt$BrowseFoldersViewModel : ViewModel
+ LibraryEntitiesShouldNotBePublic:BrowseFoldersViewModel.kt$Folder
+ LibraryEntitiesShouldNotBePublic:BundleExt.kt$@Deprecated("only for use with fragments and activities") fun Bundle?.getSpace(type: Space.Type): Pair<Space, Boolean>
+ LibraryEntitiesShouldNotBePublic:BundleExt.kt$@Deprecated("only for use with fragments and activities") fun bundleWithNewSpace()
+ LibraryEntitiesShouldNotBePublic:BundleExt.kt$@Deprecated("only for use with fragments and activities") fun bundleWithSpaceId(spaceId: Long)
+ LibraryEntitiesShouldNotBePublic:BundleExt.kt$IAResult
+ LibraryEntitiesShouldNotBePublic:ClientResult.kt$suspend fun <T> OkHttpClient.enqueueResult( request: Request, onResume: (Response) -> T )
+ LibraryEntitiesShouldNotBePublic:Collection.kt$Collection : SugarRecord
+ LibraryEntitiesShouldNotBePublic:Colors.kt$ColorTheme
+ LibraryEntitiesShouldNotBePublic:Conduit.kt$Conduit
+ LibraryEntitiesShouldNotBePublic:ConsentActivity.kt$ConsentActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:ContentPickerFragment.kt$ContentPickerFragment : BottomSheetDialogFragment
+ LibraryEntitiesShouldNotBePublic:Context.kt$fun Context.openBrowser(link: String)
+ LibraryEntitiesShouldNotBePublic:CreateNewFolderFragment.kt$CreateNewFolderFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:CustomBottomNavBar.kt$CustomBottomNavBar : LinearLayout
+ LibraryEntitiesShouldNotBePublic:CustomButton.kt$CustomButton : FrameLayout
+ LibraryEntitiesShouldNotBePublic:DefaultScaffold.kt$@Composable fun DefaultScaffold( modifier: Modifier = Modifier, topAppBar: (@Composable () -> Unit)? = null, content: @Composable () -> Unit )
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$@Composable fun DialogStateManager.showDialog(block: DialogBuilder.() -> Unit)
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$@Composable fun DialogStateManager.showSuccessDialog( message: String, title: String = "", // if empty, default title is used onPositive: () -> Unit = {} )
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$ButtonBuilder
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$ButtonData
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$DefaultResourceProvider : ResourceProvider
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$DialogBuilder
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$DialogConfig
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$DialogDsl
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$DialogType
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$ResourceProvider
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$fun DialogStateManager.showDestructiveDialog( title: UiText?, message: UiText, icon: UiImage? = null, positiveButtonText: UiText? = null, onDone: () -> Unit = {}, onCancel: () -> Unit = {} )
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$fun DialogStateManager.showDialog( resourceProvider: ResourceProvider = this.requireResourceProvider(), block: DialogBuilder.() -> Unit )
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$fun DialogStateManager.showErrorDialog( message: String, title: String = "", onRetry: () -> Unit = {}, onCancel: () -> Unit = {} )
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$fun DialogStateManager.showInfoDialog( message: UiText, title: UiText?, icon: UiImage? = null, onDone: () -> Unit = {}, )
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$fun DialogStateManager.showSuccessDialog( @StringRes title: Int?, @StringRes message: Int, @StringRes positiveButtonText: Int? = null, icon: UiImage? = null, onDone: () -> Unit = {}, )
+ LibraryEntitiesShouldNotBePublic:DialogConfigBuilder.kt$fun DialogStateManager.showWarningDialog( title: UiText?, message: UiText, icon: UiImage? = null, positiveButtonText: UiText? = null, onDone: () -> Unit = {}, onCancel: () -> Unit = {} )
+ LibraryEntitiesShouldNotBePublic:Dimensions.kt$DimensionsTheme
+ LibraryEntitiesShouldNotBePublic:Dimensions.kt$Elevations
+ LibraryEntitiesShouldNotBePublic:Dimensions.kt$Icons
+ LibraryEntitiesShouldNotBePublic:Dimensions.kt$Spacing
+ LibraryEntitiesShouldNotBePublic:Dimensions.kt$fun getThemeDimensions(isDarkTheme: Boolean)
+ LibraryEntitiesShouldNotBePublic:Dispatcher.kt$Dispatcher<Action>
+ LibraryEntitiesShouldNotBePublic:Dispatcher.kt$typealias Dispatch<A> = (A) -> Unit
+ LibraryEntitiesShouldNotBePublic:Drawable.kt$fun Drawable.clone(): Drawable?
+ LibraryEntitiesShouldNotBePublic:Drawable.kt$fun Drawable.scaled(biggerSideDipLength: Int, context: Context): Drawable
+ LibraryEntitiesShouldNotBePublic:Drawable.kt$fun Drawable.scaled(factor: Double, context: Context): Drawable
+ LibraryEntitiesShouldNotBePublic:Drawable.kt$fun Drawable.scaled(width: Int, height: Int, context: Context): Drawable
+ LibraryEntitiesShouldNotBePublic:Drawable.kt$fun Drawable.tint(color: Int): Drawable
+ LibraryEntitiesShouldNotBePublic:DrawableExtensions.kt$fun Drawable.clone(): Drawable?
+ LibraryEntitiesShouldNotBePublic:DrawableExtensions.kt$fun Drawable.scaled(biggerSideDipLength: Int, context: Context): Drawable
+ LibraryEntitiesShouldNotBePublic:DrawableExtensions.kt$fun Drawable.scaled(factor: Double, context: Context): Drawable
+ LibraryEntitiesShouldNotBePublic:DrawableExtensions.kt$fun Drawable.scaled(width: Int, height: Int, context: Context): Drawable
+ LibraryEntitiesShouldNotBePublic:DrawableExtensions.kt$fun Drawable.tint(color: Int): Drawable
+ LibraryEntitiesShouldNotBePublic:DurationExtensions.kt$fun Duration.formatToDecimalPlaces(decimals: Int = 1): String
+ LibraryEntitiesShouldNotBePublic:EditFolderActivity.kt$EditFolderActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:Effects.kt$typealias Effects<T, A> = suspend (T, A) -> Unit
+ LibraryEntitiesShouldNotBePublic:EmptyableRecyclerView.kt$EmptyableRecyclerView : RecyclerView
+ LibraryEntitiesShouldNotBePublic:ExpandableSpaceList.kt$@Composable fun DrawerSpaceListItem( space: Space, )
+ LibraryEntitiesShouldNotBePublic:ExpandableSpaceList.kt$@Composable fun ExpandableSpaceList( serverAccordionState: AccordionState, selectedSpace: Space? = null, spaceList: List<Space> )
+ LibraryEntitiesShouldNotBePublic:ExpandableSpaceList.kt$@Composable fun SpaceIcon( type: Space.Type, modifier: Modifier = Modifier, tint: Color? = null )
+ LibraryEntitiesShouldNotBePublic:FileUploadResult.kt$FileUploadResult : SerializableMarker
+ LibraryEntitiesShouldNotBePublic:FolderAdapter.kt$FolderAdapter : ListAdapterFolderAdapterListener
+ LibraryEntitiesShouldNotBePublic:FolderAdapter.kt$FolderAdapterListener
+ LibraryEntitiesShouldNotBePublic:FolderDrawerAdapter.kt$FolderDrawerAdapter : ListAdapter
+ LibraryEntitiesShouldNotBePublic:FolderDrawerAdapter.kt$FolderDrawerAdapterListener
+ LibraryEntitiesShouldNotBePublic:FolderOptionsPopup.kt$@Composable fun FolderOptionsPopup( expanded: Boolean = false, onDismissRequest: () -> Unit, onRenameFolder: () -> Unit, onSelectMedia: () -> Unit, onRemoveFolder: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:FoldersActivity.kt$FoldersActivity : BaseActivityFolderAdapterListener
+ LibraryEntitiesShouldNotBePublic:FullscreenDimmingOverlay.kt$FullScreenCreateGroupDimmingOverlay : FrameLayout
+ LibraryEntitiesShouldNotBePublic:FullscreenDimmingOverlay.kt$FullScreenDimmingOverlay : FrameLayout
+ LibraryEntitiesShouldNotBePublic:GDriveActivity.kt$GDriveActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:GDriveConduit.kt$GDriveConduit : Conduit
+ LibraryEntitiesShouldNotBePublic:GDriveFragment.kt$GDriveFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:GeneralSettingsActivity.kt$GeneralSettingsActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:HapticManager.kt$AppHapticFeedbackType
+ LibraryEntitiesShouldNotBePublic:HapticManager.kt$HapticManager
+ LibraryEntitiesShouldNotBePublic:HashingStrategy.kt$HashingStrategy
+ LibraryEntitiesShouldNotBePublic:Hbks.kt$Hbks$Availability
+ LibraryEntitiesShouldNotBePublic:Hbks.kt$Hbks$BiometryType
+ LibraryEntitiesShouldNotBePublic:HomeActivity.kt$HomeActivity : FragmentActivity
+ LibraryEntitiesShouldNotBePublic:HomeAppBar.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeAppBar( openDrawer: () -> Unit, onExit: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:HomeScreen.kt$@Composable fun HomeScreen( viewModel: HomeViewModel = koinViewModel(), onExit: () -> Unit, onNewFolder: () -> Unit, onFolderSelected: (Long) -> Unit, onAddMedia: (AddMediaType) -> Unit, onNavigateToCache: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:HomeScreen.kt$@Composable fun HomeScreenContent( onExit: () -> Unit, state: HomeScreenState, onAction: (HomeScreenAction) -> Unit, onNavigateToCache: () -> Unit = {} )
+ LibraryEntitiesShouldNotBePublic:HomeScreen.kt$@Composable fun SaveNavGraph( context: Context, viewModel: HomeViewModel = koinViewModel(), onExit: () -> Unit, onNewFolder: () -> Unit, onFolderSelected: (Long) -> Unit, onAddMedia: (AddMediaType) -> Unit )
+ LibraryEntitiesShouldNotBePublic:HomeScreen.kt$HomeScreenAction
+ LibraryEntitiesShouldNotBePublic:HomeScreen.kt$HomeScreenState
+ LibraryEntitiesShouldNotBePublic:HomeScreen.kt$HomeViewModel : ViewModel
+ LibraryEntitiesShouldNotBePublic:HttpLikeException.kt$HttpLikeException : Exception
+ LibraryEntitiesShouldNotBePublic:ISnowbirdAPI.kt$ISnowbirdAPI
+ LibraryEntitiesShouldNotBePublic:IaConduit.kt$IaConduit : Conduit
+ LibraryEntitiesShouldNotBePublic:InternetArchive.kt$InternetArchive
+ LibraryEntitiesShouldNotBePublic:InternetArchiveActivity.kt$InternetArchiveActivity : AppCompatActivity
+ LibraryEntitiesShouldNotBePublic:InternetArchiveDetailsScreen.kt$@Composable fun InternetArchiveDetailsScreen(space: Space, onResult: (IAResult) -> Unit)
+ LibraryEntitiesShouldNotBePublic:InternetArchiveDetailsState.kt$InternetArchiveDetailsState
+ LibraryEntitiesShouldNotBePublic:InternetArchiveDetailsViewModel.kt$InternetArchiveDetailsViewModel : StatefulViewModel
+ LibraryEntitiesShouldNotBePublic:InternetArchiveFragment.kt$InternetArchiveFragment : BaseFragmentToolbarConfigurable
+ LibraryEntitiesShouldNotBePublic:InternetArchiveHeader.kt$@Composable fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 18.sp)
+ LibraryEntitiesShouldNotBePublic:InternetArchiveLocalSource.kt$InternetArchiveLocalSource
+ LibraryEntitiesShouldNotBePublic:InternetArchiveLoginRequest.kt$InternetArchiveLoginRequest
+ LibraryEntitiesShouldNotBePublic:InternetArchiveLoginResponse.kt$InternetArchiveLoginResponse
+ LibraryEntitiesShouldNotBePublic:InternetArchiveLoginScreen.kt$@Composable fun CustomSecureField( modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: String, placeholder: String, isError: Boolean = false, isLoading: Boolean = false, keyboardType: KeyboardType, imeAction: ImeAction, )
+ LibraryEntitiesShouldNotBePublic:InternetArchiveLoginScreen.kt$@Composable fun CustomTextField( modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: String, enabled: Boolean = true, placeholder: String? = null, isError: Boolean = false, isLoading: Boolean = false, keyboardType: KeyboardType = KeyboardType.Text, imeAction: ImeAction = ImeAction.Next, )
+ LibraryEntitiesShouldNotBePublic:InternetArchiveLoginScreen.kt$@Composable fun InternetArchiveLoginScreen(space: Space, onResult: (IAResult) -> Unit)
+ LibraryEntitiesShouldNotBePublic:InternetArchiveLoginScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun ComposeAppBar( title: String = "Save App", onNavigationAction: () -> Unit = {} )
+ LibraryEntitiesShouldNotBePublic:InternetArchiveLoginState.kt$InternetArchiveLoginAction
+ LibraryEntitiesShouldNotBePublic:InternetArchiveLoginState.kt$InternetArchiveLoginState
+ LibraryEntitiesShouldNotBePublic:InternetArchiveLoginUseCase.kt$InternetArchiveLoginUseCase
+ LibraryEntitiesShouldNotBePublic:InternetArchiveLoginViewModel.kt$InternetArchiveLoginViewModel : StatefulViewModelKoinComponent
+ LibraryEntitiesShouldNotBePublic:InternetArchiveMapper.kt$InternetArchiveMapper
+ LibraryEntitiesShouldNotBePublic:InternetArchiveRemoteSource.kt$InternetArchiveRemoteSource
+ LibraryEntitiesShouldNotBePublic:InternetArchiveRepository.kt$InternetArchiveRepository
+ LibraryEntitiesShouldNotBePublic:InternetArchiveScreen.kt$@Composable fun InternetArchiveScreen(space: Space, isNewSpace: Boolean, onFinish: (IAResult) -> Unit)
+ LibraryEntitiesShouldNotBePublic:JoinGroupResponse.kt$JoinGroupResponse : SerializableMarker
+ LibraryEntitiesShouldNotBePublic:Listener.kt$Listener<Action>
+ LibraryEntitiesShouldNotBePublic:MainActivity.kt$MainActivity : BaseActivitySpaceDrawerAdapterListenerFolderDrawerAdapterListener
+ LibraryEntitiesShouldNotBePublic:MainBottomBar.kt$@Composable fun MainBottomBar( isSettings: Boolean, onMyMediaClick: () -> Unit, onSettingsClick: () -> Unit, onAddMediaClick: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:MainBottomBar.kt$@Composable fun RowScope.BottomNavMenuItem( selectedIcon: ImageVector, unSelectedIcon: ImageVector, isSelected: Boolean, text: String, onClick: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:MainDrawerContent.kt$@Composable fun MainDrawerContent( selectedSpace: Space? = null, spaceList: List<Space> = emptyList() )
+ LibraryEntitiesShouldNotBePublic:MainDrawerContent.kt$@Composable fun MainDrawerFolderListItem( project: Project, isSelected: Boolean = false, onSelected: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:MainMediaAdapter.kt$MainMediaAdapter : Adapter
+ LibraryEntitiesShouldNotBePublic:MainMediaAdapterTest.kt$MainMediaAdapterTest
+ LibraryEntitiesShouldNotBePublic:MainMediaAdapterTest.kt$fun createTestMedia( id: Long, uri: String, status: Media.Status, progress: Int? = 0, selected: Boolean = false, title: String = "Test Media" ): Media
+ LibraryEntitiesShouldNotBePublic:MainMediaFragment.kt$MainMediaFragment : Fragment
+ LibraryEntitiesShouldNotBePublic:MainMediaScreen.kt$@Composable fun CollectionHeaderView(section: CollectionSection)
+ LibraryEntitiesShouldNotBePublic:MainMediaScreen.kt$@Composable fun CollectionSectionView( section: CollectionSection, onMediaClick: (Media) -> Unit, onMediaLongPress: (Media) -> Unit )
+ LibraryEntitiesShouldNotBePublic:MainMediaScreen.kt$@Composable fun ErrorIndicator()
+ LibraryEntitiesShouldNotBePublic:MainMediaScreen.kt$@Composable fun MainMediaScreen( projectId: Long, )
+ LibraryEntitiesShouldNotBePublic:MainMediaScreen.kt$@Composable fun MediaItemView( media: Media, isSelected: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier )
+ LibraryEntitiesShouldNotBePublic:MainMediaScreen.kt$@Composable fun UploadProgress(progress: Int)
+ LibraryEntitiesShouldNotBePublic:MainMediaScreen.kt$@Composable fun WelcomeMessage()
+ LibraryEntitiesShouldNotBePublic:MainMediaScreen.kt$CollectionSection
+ LibraryEntitiesShouldNotBePublic:MainMediaViewHolder.kt$MainMediaViewHolder : ViewHolder
+ LibraryEntitiesShouldNotBePublic:MainMediaViewModel.kt$MainMediaViewModel : ViewModel
+ LibraryEntitiesShouldNotBePublic:MainViewModel.kt$MainUiState
+ LibraryEntitiesShouldNotBePublic:MainViewModel.kt$MainViewModel : ViewModel
+ LibraryEntitiesShouldNotBePublic:Media.kt$Media : SugarRecord
+ LibraryEntitiesShouldNotBePublic:MediaAdapter.kt$MediaAdapter : Adapter
+ LibraryEntitiesShouldNotBePublic:MediaAdapter.kt$MediaDiffCallback : Callback
+ LibraryEntitiesShouldNotBePublic:MediaCacheScreen.kt$@Composable fun CacheFileItem(file: MediaFile)
+ LibraryEntitiesShouldNotBePublic:MediaCacheScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaCacheScreen(context: Context, onNavigateBack: () -> Unit)
+ LibraryEntitiesShouldNotBePublic:MediaCacheScreen.kt$FileType
+ LibraryEntitiesShouldNotBePublic:MediaCacheScreen.kt$MediaFile
+ LibraryEntitiesShouldNotBePublic:MediaCacheScreen.kt$fun File.toMediaFile(): MediaFile
+ LibraryEntitiesShouldNotBePublic:MediaLaunchers.kt$MediaLaunchers
+ LibraryEntitiesShouldNotBePublic:MediaViewHolder.kt$MediaViewHolder : ViewHolder
+ LibraryEntitiesShouldNotBePublic:Module.kt$typealias InternetArchiveGson = Gson
+ LibraryEntitiesShouldNotBePublic:Notifier.kt$Notifier<Action>
+ LibraryEntitiesShouldNotBePublic:Notifier.kt$typealias Notify<A> = suspend (A) -> Unit
+ LibraryEntitiesShouldNotBePublic:NumericKeypad.kt$@Composable fun NumericKeypad( isEnabled: Boolean = true, onNumberClick: (String) -> Unit, onDeleteClick: () -> Unit, onSubmitClick: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:Onboarding23Activity.kt$Onboarding23Activity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:Onboarding23FragmentStateAdapter.kt$Onboarding23FragmentStateAdapter : FragmentStateAdapter
+ LibraryEntitiesShouldNotBePublic:Onboarding23InstructionsActivity.kt$Onboarding23InstructionsActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:Onboarding23SlideFragment.kt$Onboarding23SlideFragment : Fragment
+ LibraryEntitiesShouldNotBePublic:PBKDF2HashingStrategy.kt$PBKDF2HashingStrategy : HashingStrategy
+ LibraryEntitiesShouldNotBePublic:PackageManager.kt$fun PackageManager.getVersionName(packageName: String): String
+ LibraryEntitiesShouldNotBePublic:PasscodeDots.kt$@Composable fun PasscodeDots( passcodeLength: Int, currentPasscodeLength: Int, shouldShake: Boolean = false )
+ LibraryEntitiesShouldNotBePublic:PasscodeEntryActivity.kt$PasscodeEntryActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:PasscodeEntryScreen.kt$@Composable fun PasscodeEntryScreen( onPasscodeSuccess: () -> Unit, onExit: () -> Unit, viewModel: PasscodeEntryViewModel = koinViewModel(), hapticManager: HapticManager = koinInject() )
+ LibraryEntitiesShouldNotBePublic:PasscodeEntryScreen.kt$@Composable fun PasscodeEntryScreenContent( state: PasscodeEntryScreenState, onAction: (PasscodeEntryScreenAction) -> Unit, onExit: () -> Unit, )
+ LibraryEntitiesShouldNotBePublic:PasscodeEntryViewModel.kt$PasscodeEntryScreenAction
+ LibraryEntitiesShouldNotBePublic:PasscodeEntryViewModel.kt$PasscodeEntryScreenState
+ LibraryEntitiesShouldNotBePublic:PasscodeEntryViewModel.kt$PasscodeEntryUiEvent
+ LibraryEntitiesShouldNotBePublic:PasscodeEntryViewModel.kt$PasscodeEntryViewModel : ViewModel
+ LibraryEntitiesShouldNotBePublic:PasscodeManager.kt$PasscodeManager : ActivityLifecycleCallbacks
+ LibraryEntitiesShouldNotBePublic:PasscodeRepository.kt$PasscodeRepository
+ LibraryEntitiesShouldNotBePublic:PasscodeSetupActivity.kt$PasscodeSetupActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:PasscodeSetupScreen.kt$@Composable fun PasscodeSetupScreen( onPasscodeSet: () -> Unit, onCancel: () -> Unit, viewModel: PasscodeSetupViewModel = koinViewModel(), hapticManager: HapticManager = koinInject() )
+ LibraryEntitiesShouldNotBePublic:PasscodeSetupViewModel.kt$PasscodeSetupUiAction
+ LibraryEntitiesShouldNotBePublic:PasscodeSetupViewModel.kt$PasscodeSetupUiEvent
+ LibraryEntitiesShouldNotBePublic:PasscodeSetupViewModel.kt$PasscodeSetupUiState
+ LibraryEntitiesShouldNotBePublic:PasscodeSetupViewModel.kt$PasscodeSetupViewModel : ViewModel
+ LibraryEntitiesShouldNotBePublic:Preview.kt$@Composable fun DefaultBoxPreview( content: @Composable () -> Unit )
+ LibraryEntitiesShouldNotBePublic:Preview.kt$@Composable fun DefaultEmptyScaffoldPreview( content: @Composable () -> Unit )
+ LibraryEntitiesShouldNotBePublic:Preview.kt$@Composable fun DefaultScaffoldPreview( content: @Composable () -> Unit )
+ LibraryEntitiesShouldNotBePublic:PreviewActivity.kt$PreviewActivity : BaseActivityOnClickListenerListener
+ LibraryEntitiesShouldNotBePublic:PreviewAdapter.kt$PreviewAdapter : ListAdapter
+ LibraryEntitiesShouldNotBePublic:PreviewViewHolder.kt$PreviewViewHolder : ViewHolder
+ LibraryEntitiesShouldNotBePublic:PrimaryButton.kt$@Composable fun PrimaryButton( modifier: Modifier = Modifier, icon: ImageVector? = null, text: String, onClick: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:ProcessingTracker.kt$ProcessingTracker
+ LibraryEntitiesShouldNotBePublic:ProcessingTracker.kt$suspend fun <T> ProcessingTracker.trackProcessing( taskName: String = "Unnamed task", block: suspend () -> T ): T
+ LibraryEntitiesShouldNotBePublic:ProcessingTracker.kt$suspend fun <T> ProcessingTracker.trackProcessingWithTimeout( timeoutMs: Long, taskName: String = "Unnamed task", block: suspend () -> T ): T
+ LibraryEntitiesShouldNotBePublic:Project.kt$Project : SugarRecord
+ LibraryEntitiesShouldNotBePublic:ProjectAdapter.kt$ProjectAdapter : FragmentStateAdapter
+ LibraryEntitiesShouldNotBePublic:ProofModeScreen.kt$@Composable fun ProofModeScreen( onNavigateBack: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:ProofModeScreen.kt$@Composable fun ProofModeScreenContent()
+ LibraryEntitiesShouldNotBePublic:ProofModeSettingsActivity.kt$ProofModeSettingsActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:QRScannerActivity.kt$QRScannerActivity : CaptureActivity
+ LibraryEntitiesShouldNotBePublic:Reducer.kt$fun <T, A> MutableStateFlow<T>.apply(action: A, reducer: Reducer<T, A>)
+ LibraryEntitiesShouldNotBePublic:Reducer.kt$typealias Reducer<T, A> = (T, A) -> T
+ LibraryEntitiesShouldNotBePublic:RequestBodyUtil.kt$fun createListener( cancellable: () -> Boolean, onProgress: (Long) -> Unit = { }, onComplete: () -> Unit = {} )
+ LibraryEntitiesShouldNotBePublic:RequestListener.kt$RequestListener
+ LibraryEntitiesShouldNotBePublic:RequestNameDTO.kt$MembershipRequest : SerializableMarker
+ LibraryEntitiesShouldNotBePublic:RequestNameDTO.kt$RequestName : SerializableMarker
+ LibraryEntitiesShouldNotBePublic:RestEndpointTask.kt$RestEndpointTask : Runnable
+ LibraryEntitiesShouldNotBePublic:RetrofitAPI.kt$RetrofitAPI : ISnowbirdAPI
+ LibraryEntitiesShouldNotBePublic:RetrofitClient.kt$RetrofitClient
+ LibraryEntitiesShouldNotBePublic:RetryConfig.kt$RetryConfig
+ LibraryEntitiesShouldNotBePublic:ReviewActivity.kt$ReviewActivity : BaseActivityOnClickListener
+ LibraryEntitiesShouldNotBePublic:SaveApp.kt$SaveApp : SugarAppFactory
+ LibraryEntitiesShouldNotBePublic:SaveClient.kt$SaveClient : StrongBuilderBase
+ LibraryEntitiesShouldNotBePublic:ScryptHashingStrategy.kt$ScryptHashingStrategy : HashingStrategy
+ LibraryEntitiesShouldNotBePublic:SectionViewHolder.kt$SectionViewHolder
+ LibraryEntitiesShouldNotBePublic:SerializableMarker.kt$SerializableMarker
+ LibraryEntitiesShouldNotBePublic:ServerOptionItem.kt$@Composable fun ServerOptionItem( @DrawableRes iconRes: Int, title: String, subtitle: String, onClick: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:SettingsFragment.kt$SettingsFragment : PreferenceFragmentCompat
+ LibraryEntitiesShouldNotBePublic:SettingsScreen.kt$@Composable fun SettingsScreen( onNavigateToCache: () -> Unit = {} )
+ LibraryEntitiesShouldNotBePublic:SmartFragmentStatePagerAdapter.kt$SmartFragmentStatePagerAdapter : FragmentStatePagerAdapter
+ LibraryEntitiesShouldNotBePublic:SnowbirdBridge.kt$SnowbirdBridge
+ LibraryEntitiesShouldNotBePublic:SnowbirdConduit.kt$SnowbirdConduit : Conduit
+ LibraryEntitiesShouldNotBePublic:SnowbirdCreateGroupFragment.kt$SnowbirdCreateGroupFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:SnowbirdError.kt$SnowbirdError : SerializableMarker
+ LibraryEntitiesShouldNotBePublic:SnowbirdFileItem.kt$SnowbirdFileItem : SugarRecordSerializableMarker
+ LibraryEntitiesShouldNotBePublic:SnowbirdFileItem.kt$SnowbirdFileList : SerializableMarker
+ LibraryEntitiesShouldNotBePublic:SnowbirdFileListAdapter.kt$SnowbirdFileDiffCallback : ItemCallback
+ LibraryEntitiesShouldNotBePublic:SnowbirdFileListAdapter.kt$SnowbirdFileListAdapter : ListAdapter
+ LibraryEntitiesShouldNotBePublic:SnowbirdFileListAdapter.kt$SnowbirdFileViewHolder : ViewHolder
+ LibraryEntitiesShouldNotBePublic:SnowbirdFileListFragment.kt$SnowbirdFileListFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:SnowbirdFileRepository.kt$ISnowbirdFileRepository
+ LibraryEntitiesShouldNotBePublic:SnowbirdFileRepository.kt$SnowbirdFileRepository : ISnowbirdFileRepository
+ LibraryEntitiesShouldNotBePublic:SnowbirdFileViewModel.kt$SnowbirdFileViewModel : BaseViewModel
+ LibraryEntitiesShouldNotBePublic:SnowbirdFragment.kt$SnowbirdFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:SnowbirdGroup.kt$SnowbirdGroup : SugarRecordSerializableMarker
+ LibraryEntitiesShouldNotBePublic:SnowbirdGroup.kt$SnowbirdGroupList : SerializableMarker
+ LibraryEntitiesShouldNotBePublic:SnowbirdGroup.kt$fun SnowbirdGroup.shortHash(): String
+ LibraryEntitiesShouldNotBePublic:SnowbirdGroupListAdapter.kt$SnowbirdGroupsAdapter : ListAdapter
+ LibraryEntitiesShouldNotBePublic:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:SnowbirdGroupOverviewFragment.kt$SnowbirdGroupOverviewFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:SnowbirdGroupRepository.kt$ISnowbirdGroupRepository
+ LibraryEntitiesShouldNotBePublic:SnowbirdGroupRepository.kt$SnowbirdGroupRepository : ISnowbirdGroupRepository
+ LibraryEntitiesShouldNotBePublic:SnowbirdGroupViewModel.kt$SnowbirdGroupViewModel : BaseViewModel
+ LibraryEntitiesShouldNotBePublic:SnowbirdJoinGroupFragment.kt$SnowbirdJoinGroupFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:SnowbirdRepo.kt$SnowbirdRepo : SugarRecordSerializableMarker
+ LibraryEntitiesShouldNotBePublic:SnowbirdRepo.kt$SnowbirdRepoList : SerializableMarker
+ LibraryEntitiesShouldNotBePublic:SnowbirdRepo.kt$fun SnowbirdRepo.shortHash(): String
+ LibraryEntitiesShouldNotBePublic:SnowbirdRepoListAdapter.kt$SnowbirdRepoListAdapter : ListAdapter
+ LibraryEntitiesShouldNotBePublic:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:SnowbirdRepoRepository.kt$ISnowbirdRepoRepository
+ LibraryEntitiesShouldNotBePublic:SnowbirdRepoRepository.kt$SnowbirdRepoRepository : ISnowbirdRepoRepository
+ LibraryEntitiesShouldNotBePublic:SnowbirdRepoViewModel.kt$SnowbirdRepoViewModel : BaseViewModel
+ LibraryEntitiesShouldNotBePublic:SnowbirdResult.kt$SnowbirdResult<out T>
+ LibraryEntitiesShouldNotBePublic:SnowbirdService.kt$ServiceStatus
+ LibraryEntitiesShouldNotBePublic:SnowbirdService.kt$SnowbirdService : Service
+ LibraryEntitiesShouldNotBePublic:SnowbirdServiceStatus.kt$SnowbirdServiceStatus
+ LibraryEntitiesShouldNotBePublic:SnowbirdShareFragment.kt$SnowbirdShareFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:Space.kt$Space : SugarRecord
+ LibraryEntitiesShouldNotBePublic:SpaceAdapter.kt$SpaceAdapter : ListAdapterSpaceAdapterListener
+ LibraryEntitiesShouldNotBePublic:SpaceAdapter.kt$SpaceAdapterListener
+ LibraryEntitiesShouldNotBePublic:SpaceAdapter.kt$SpaceItemDecoration : ItemDecoration
+ LibraryEntitiesShouldNotBePublic:SpaceDrawerAdapter.kt$SpaceDrawerAdapter : ListAdapter
+ LibraryEntitiesShouldNotBePublic:SpaceDrawerAdapter.kt$SpaceDrawerAdapterListener
+ LibraryEntitiesShouldNotBePublic:SpaceListFragment.kt$SpaceListFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:SpaceListScreen.kt$@Composable fun SpaceListItem( space: Space, onClick: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:SpaceListScreen.kt$@Composable fun SpaceListScreen( onSpaceClicked: (Space) -> Unit, )
+ LibraryEntitiesShouldNotBePublic:SpaceListScreen.kt$@Composable fun SpaceListScreenContent( onSpaceClicked: (Space) -> Unit, spaceList: List<Space> = emptyList() )
+ LibraryEntitiesShouldNotBePublic:SpaceSetupActivity.kt$SpaceSetupActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:SpaceSetupActivity.kt$StartDestination
+ LibraryEntitiesShouldNotBePublic:SpaceSetupFragment.kt$SpaceSetupFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:SpaceSetupScreen.kt$@Composable fun SpaceSetupScreen( onWebDavClick: () -> Unit, isInternetArchiveAllowed: Boolean, onInternetArchiveClick: () -> Unit, isDwebEnabled: Boolean, onDwebClicked: () -> Unit )
+ LibraryEntitiesShouldNotBePublic:SpaceSetupSuccessFragment.kt$SpaceSetupSuccessFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:SpacingItemDecoration.kt$SpacingItemDecoration : ItemDecoration
+ LibraryEntitiesShouldNotBePublic:StateDispatcher.kt$StateDispatcher<T, A> : DispatcherStateful
+ LibraryEntitiesShouldNotBePublic:Stateful.kt$Stateful<T>
+ LibraryEntitiesShouldNotBePublic:StatefulViewModel.kt$StatefulViewModel<State, Action> : ViewModelStoreStateful
+ LibraryEntitiesShouldNotBePublic:Store.kt$Store<Action> : DispatcherListenerNotifier
+ LibraryEntitiesShouldNotBePublic:StoreObserver.kt$StoreObserver<T> : NotifierListener
+ LibraryEntitiesShouldNotBePublic:StringExtensions.kt$fun String.asQRCode(size: Int = 512, quietZone: Int = 4): Bitmap
+ LibraryEntitiesShouldNotBePublic:StringExtensions.kt$fun String.createInputStream(): InputStream?
+ LibraryEntitiesShouldNotBePublic:StringExtensions.kt$fun String.getQueryParameter(paramName: String): String?
+ LibraryEntitiesShouldNotBePublic:StringExtensions.kt$fun String.isValidUrl()
+ LibraryEntitiesShouldNotBePublic:StringExtensions.kt$fun String.uriToPath(): String
+ LibraryEntitiesShouldNotBePublic:StringExtensions.kt$fun String.urlEncode(): String
+ LibraryEntitiesShouldNotBePublic:SuspendableExtensions.kt$RetryAttempt<out T>
+ LibraryEntitiesShouldNotBePublic:SuspendableExtensions.kt$RetryResult<out T>
+ LibraryEntitiesShouldNotBePublic:SuspendableExtensions.kt$fun <T> (suspend () -> T).retryWithScope( scope: CoroutineScope, config: RetryConfig, shouldRetry: (Throwable) -> Boolean = { true }, onEach: (RetryAttempt<T>) -> Unit ): Job
+ LibraryEntitiesShouldNotBePublic:SuspendableExtensions.kt$fun <T> (suspend () -> T).withRetry( config: RetryConfig, shouldRetry: (Throwable) -> Boolean = { true } ): Flow<RetryAttempt<T>>
+ LibraryEntitiesShouldNotBePublic:SuspendableExtensions.kt$fun <T> suspendToRetry(block: suspend () -> T): suspend () -> T
+ LibraryEntitiesShouldNotBePublic:SwipeToDeleteCallback.kt$SwipeToDeleteCallback : Callback
+ LibraryEntitiesShouldNotBePublic:TextView.kt$Position
+ LibraryEntitiesShouldNotBePublic:TextView.kt$fun TextView.scaleAndTintDrawable(position: Position, scale: Double = 1.0, tint: Boolean = true)
+ LibraryEntitiesShouldNotBePublic:TextView.kt$fun TextView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawables: List<Drawable?>)
+ LibraryEntitiesShouldNotBePublic:TextView.kt$fun TextView.setDrawable(drawable: Drawable?, position: Position, scale: Double = 1.0, tint: Boolean = true)
+ LibraryEntitiesShouldNotBePublic:TextView.kt$fun TextView.setDrawable(id: Int, position: Position, scale: Double = 1.0, tint: Boolean = true)
+ LibraryEntitiesShouldNotBePublic:TextView.kt$fun TextView.styleAsLink()
+ LibraryEntitiesShouldNotBePublic:Theme.kt$@Composable fun SaveAppTheme( content: @Composable () -> Unit )
+ LibraryEntitiesShouldNotBePublic:Theme.kt$Theme
+ LibraryEntitiesShouldNotBePublic:ThrowableExceptions.kt$fun Throwable.toSnowbirdError(): SnowbirdError
+ LibraryEntitiesShouldNotBePublic:ToolbarConfigurable.kt$ToolbarConfigurable
+ LibraryEntitiesShouldNotBePublic:TorStatusContentProvider.kt$TorStatusContentProvider : ContentProvider
+ LibraryEntitiesShouldNotBePublic:TorStatusDatabase.kt$TorStatusDatabase : SQLiteOpenHelper
+ LibraryEntitiesShouldNotBePublic:TwoLetterDrawable.kt$TwoLetterDrawable : Drawable
+ LibraryEntitiesShouldNotBePublic:UiImage.kt$UiImage
+ LibraryEntitiesShouldNotBePublic:UiImage.kt$fun @receiver:DrawableRes Int.asUiImage(): UiImage.DrawableResource
+ LibraryEntitiesShouldNotBePublic:UiImage.kt$fun ImageVector.asUiImage(): UiImage
+ LibraryEntitiesShouldNotBePublic:UiText.kt$UiText
+ LibraryEntitiesShouldNotBePublic:UiText.kt$fun @receiver:StringRes Int.asUiText(): UiText
+ LibraryEntitiesShouldNotBePublic:UiText.kt$fun String.asUiText(): UiText
+ LibraryEntitiesShouldNotBePublic:UnauthenticatedException.kt$UnauthenticatedException : RuntimeException
+ LibraryEntitiesShouldNotBePublic:UnitTests.kt$UnitTests
+ LibraryEntitiesShouldNotBePublic:UnixSocketAPI.kt$UnixSocketAPI : ISnowbirdAPI
+ LibraryEntitiesShouldNotBePublic:UnixSocketClient.kt$HttpMethod
+ LibraryEntitiesShouldNotBePublic:UnixSocketClient.kt$UnixSocketClient
+ LibraryEntitiesShouldNotBePublic:UnixSocketClientFileExtensions.kt$suspend fun UnixSocketClient.downloadFile(endpoint: String): ByteArray
+ LibraryEntitiesShouldNotBePublic:UnixSocketClientFileExtensions.kt$suspend inline fun <reified RESPONSE : SerializableMarker> UnixSocketClient.uploadFile( endpoint: String, imageData: ByteArray ): RESPONSE
+ LibraryEntitiesShouldNotBePublic:UnixSocketClientFileExtensions.kt$suspend inline fun <reified RESPONSE : SerializableMarker> UnixSocketClient.uploadFile( endpoint: String, inputStream: InputStream, ): RESPONSE
+ LibraryEntitiesShouldNotBePublic:UnixSocketClientUtilityExtensions.kt$suspend fun UnixSocketClient.readBinaryResponseWithCancellation( inputStream: InputStream, onProgress: ((Long) -> Unit)? = null ): Triple<Int, Map<String, String>, ByteArray>
+ LibraryEntitiesShouldNotBePublic:UploadManagerActivity.kt$UploadManagerActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:UploadManagerFragment.kt$UploadManagerFragment : BottomSheetDialogFragment
+ LibraryEntitiesShouldNotBePublic:UploadService.kt$UploadService : JobService
+ LibraryEntitiesShouldNotBePublic:UriExtensions.kt$fun Uri.createInputStream(applicationContext: Context): InputStream?
+ LibraryEntitiesShouldNotBePublic:UriExtensions.kt$fun Uri.getFilename(applicationContext: Context): String?
+ LibraryEntitiesShouldNotBePublic:Util.kt$Util$RandomString
+ LibraryEntitiesShouldNotBePublic:ValidateLoginCredentialsUseCase.kt$ValidateLoginCredentialsUseCase
+ LibraryEntitiesShouldNotBePublic:VideoRequestHandler.kt$VideoRequestHandler : RequestHandler
+ LibraryEntitiesShouldNotBePublic:View.kt$fun View.cloak(animate: Boolean = false)
+ LibraryEntitiesShouldNotBePublic:View.kt$fun View.disableAnimation(around: () -> Unit)
+ LibraryEntitiesShouldNotBePublic:View.kt$fun View.hide(animate: Boolean = false)
+ LibraryEntitiesShouldNotBePublic:View.kt$fun View.makeSnackBar(message: CharSequence, duration: Int = Snackbar.LENGTH_INDEFINITE): Snackbar
+ LibraryEntitiesShouldNotBePublic:View.kt$fun View.show(animate: Boolean = false)
+ LibraryEntitiesShouldNotBePublic:View.kt$fun View.toggle(state: Boolean? = null, animate: Boolean = false)
+ LibraryEntitiesShouldNotBePublic:ViewExtension.kt$fun View.cloak(animate: Boolean = false)
+ LibraryEntitiesShouldNotBePublic:ViewExtension.kt$fun View.disableAnimation(around: () -> Unit)
+ LibraryEntitiesShouldNotBePublic:ViewExtension.kt$fun View.getMeasurments(): Pair<Int, Int>
+ LibraryEntitiesShouldNotBePublic:ViewExtension.kt$fun View.hide(animate: Boolean = false)
+ LibraryEntitiesShouldNotBePublic:ViewExtension.kt$fun View.makeSnackBar(message: CharSequence, duration: Int = Snackbar.LENGTH_INDEFINITE): Snackbar
+ LibraryEntitiesShouldNotBePublic:ViewExtension.kt$fun View.propagateClickToParent()
+ LibraryEntitiesShouldNotBePublic:ViewExtension.kt$fun View.show(animate: Boolean = false)
+ LibraryEntitiesShouldNotBePublic:ViewExtension.kt$fun View.showKeyboard()
+ LibraryEntitiesShouldNotBePublic:ViewExtension.kt$fun View.toggle(state: Boolean? = null, animate: Boolean = false)
+ LibraryEntitiesShouldNotBePublic:WebDAVModel.kt$BackendCapabilities
+ LibraryEntitiesShouldNotBePublic:WebDAVModel.kt$Data
+ LibraryEntitiesShouldNotBePublic:WebDAVModel.kt$Meta
+ LibraryEntitiesShouldNotBePublic:WebDAVModel.kt$Ocs
+ LibraryEntitiesShouldNotBePublic:WebDAVModel.kt$Quota
+ LibraryEntitiesShouldNotBePublic:WebDAVModel.kt$WebDAVModel
+ LibraryEntitiesShouldNotBePublic:WebDavActivity.kt$WebDavActivity : BaseActivity
+ LibraryEntitiesShouldNotBePublic:WebDavConduit.kt$WebDavConduit : Conduit
+ LibraryEntitiesShouldNotBePublic:WebDavFragment.kt$WebDavFragment : BaseFragment
+ LibraryEntitiesShouldNotBePublic:WebDavSetupLicenseFragment.kt$WebDavSetupLicenseFragment : BaseFragment
+ LongMethod:BaseDialog.kt$@Composable fun BaseDialog( onDismiss: () -> Unit, icon: UiImage? = null, iconColor: Color? = null, title: String, message: String, hasCheckbox: Boolean = false, onCheckBoxStateChanged: (Boolean) -> Unit = {}, checkBoxHint: String = "Do not show me this again", positiveButton: ButtonData? = null, neutralButton: ButtonData? = null, destructiveButton: ButtonData? = null, backgroundColor: Color = MaterialTheme.colorScheme.surface )
+ LongMethod:FileUtils.kt$FileUtils$@SuppressLint("NewAPI", "LogNotTimber") fun getPath(context: Context, uri: Uri): String?
+ LongMethod:HomeScreen.kt$@Composable fun HomeScreenContent( onExit: () -> Unit, state: HomeScreenState, onAction: (HomeScreenAction) -> Unit, onNavigateToCache: () -> Unit = {} )
+ LongMethod:InternetArchiveDetailsScreen.kt$@Composable private fun InternetArchiveDetailsContent( state: InternetArchiveDetailsState, dispatch: Dispatch<Action>, dialogManager: DialogStateManager = koinViewModel() )
+ LongMethod:InternetArchiveLoginScreen.kt$@Composable private fun InternetArchiveLoginContent( state: InternetArchiveLoginState, dispatch: Dispatch<Action> )
+ LongMethod:MainDrawerContent.kt$@Composable fun MainDrawerContent( selectedSpace: Space? = null, spaceList: List<Space> = emptyList() )
+ LongMethod:MainMediaViewHolder.kt$MainMediaViewHolder$fun bind(media: Media? = null, isInSelectionMode: Boolean = false, doImageFade: Boolean = true)
+ LongMethod:MediaAdapter.kt$MediaAdapter$override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder
+ LongMethod:MediaViewHolder.kt$MediaViewHolder$@SuppressLint("SetTextI18n") fun bind(media: Media? = null, batchMode: Boolean = false, doImageFade: Boolean = true)
+ LongMethod:NumericKeypad.kt$@Composable private fun NumberButton( label: String, enabled: Boolean = true, onClick: () -> Unit, hapticManager: HapticManager = koinInject() )
+ LongMethod:PasscodeSetupScreen.kt$@Composable private fun PasscodeSetupScreenContent( state: PasscodeSetupUiState, onAction: (PasscodeSetupUiAction) -> Unit )
+ LongMethod:PreviewViewHolder.kt$PreviewViewHolder$@SuppressLint("SetTextI18n") fun bind(media: Media? = null, batchMode: Boolean = false, doImageFade: Boolean = true)
+ LongMethod:ProofModeScreen.kt$@Composable fun ProofModeScreenContent()
+ LongMethod:ProofModeSettingsActivity.kt$ProofModeSettingsActivity.Fragment$override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?)
+ LongMethod:SettingsFragment.kt$SettingsFragment$override fun onCreatePreferences( savedInstanceState: Bundle?, rootKey: String? )
+ LongMethod:SettingsScreen.kt$@Composable fun SettingsScreen( onNavigateToCache: () -> Unit = {} )
+ LongMethod:UnixSocketClientUtilityExtensions.kt$suspend fun UnixSocketClient.readBinaryResponseWithCancellation( inputStream: InputStream, onProgress: ((Long) -> Unit)? = null ): Triple<Int, Map<String, String>, ByteArray>
+ LongMethod:WebDavConduit.kt$WebDavConduit$@Throws(IOException::class) private suspend fun uploadChunked(base: HttpUrl, path: List<String>, fileName: String): Boolean
+ LongMethod:WebDavFragment.kt$WebDavFragment$override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View
+ LongParameterList:Accordion.kt$( modifier: Modifier = Modifier, headerModifier: Modifier = Modifier, state: AccordionState = rememberAccordionState(), animate: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, headerContent: @Composable () -> Unit, bodyContent: @Composable () -> Unit, )
+ LongParameterList:BaseButton.kt$( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, backgroundColor: Color = MaterialTheme.colorScheme.primary, textColor: Color = MaterialTheme.colorScheme.onPrimary, cornerRadius: Dp = 12.dp, )
+ LongParameterList:BaseButton.kt$( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, borderColor: Color = MaterialTheme.colorScheme.error, textColor: Color = MaterialTheme.colorScheme.error, cornerRadius: Dp = 12.dp, )
+ LongParameterList:BaseDialog.kt$( onDismiss: () -> Unit, icon: UiImage? = null, iconColor: Color? = null, title: String, message: String, hasCheckbox: Boolean = false, onCheckBoxStateChanged: (Boolean) -> Unit = {}, checkBoxHint: String = "Do not show me this again", positiveButton: ButtonData? = null, neutralButton: ButtonData? = null, destructiveButton: ButtonData? = null, backgroundColor: Color = MaterialTheme.colorScheme.surface )
+ LongParameterList:DialogConfigBuilder.kt$( title: UiText?, message: UiText, icon: UiImage? = null, positiveButtonText: UiText? = null, onDone: () -> Unit = {}, onCancel: () -> Unit = {} )
+ LongParameterList:HomeScreen.kt$( context: Context, viewModel: HomeViewModel = koinViewModel(), onExit: () -> Unit, onNewFolder: () -> Unit, onFolderSelected: (Long) -> Unit, onAddMedia: (AddMediaType) -> Unit )
+ LongParameterList:HomeScreen.kt$( viewModel: HomeViewModel = koinViewModel(), onExit: () -> Unit, onNewFolder: () -> Unit, onFolderSelected: (Long) -> Unit, onAddMedia: (AddMediaType) -> Unit, onNavigateToCache: () -> Unit )
+ LongParameterList:InternetArchiveLoginScreen.kt$( modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: String, enabled: Boolean = true, placeholder: String? = null, isError: Boolean = false, isLoading: Boolean = false, keyboardType: KeyboardType = KeyboardType.Text, imeAction: ImeAction = ImeAction.Next, )
+ LongParameterList:InternetArchiveLoginScreen.kt$( modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: String, placeholder: String, isError: Boolean = false, isLoading: Boolean = false, keyboardType: KeyboardType, imeAction: ImeAction, )
+ LongParameterList:MainMediaAdapterTest.kt$( id: Long, uri: String, status: Media.Status, progress: Int? = 0, selected: Boolean = false, title: String = "Test Media" )
+ LongParameterList:Utility.kt$Utility$( context: Context, title: String, message: String? = null, positiveButtonText: String, negativeButtonText: String, completion: (Boolean) -> Unit )
+ LoopWithTooManyJumpStatements:SuspendableExtensions.kt$while
+ LoopWithTooManyJumpStatements:UnixSocketClientUtilityExtensions.kt$while
+ MagicNumber:BadgeDrawable.kt$BadgeDrawable$4
+ MagicNumber:BadgeDrawable.kt$BadgeDrawable$5f
+ MagicNumber:Colors.kt$0xff000A0A
+ MagicNumber:Colors.kt$0xff001b19
+ MagicNumber:Colors.kt$0xff003530
+ MagicNumber:Colors.kt$0xff004e48
+ MagicNumber:Colors.kt$0xff00685f
+ MagicNumber:Colors.kt$0xff008177
+ MagicNumber:Colors.kt$0xff009b8f
+ MagicNumber:Colors.kt$0xff00b4a6
+ MagicNumber:Colors.kt$0xff00cebe
+ MagicNumber:Colors.kt$0xff00e7d5
+ MagicNumber:Colors.kt$0xff00ffeb
+ MagicNumber:Colors.kt$0xff101010
+ MagicNumber:Colors.kt$0xff212021
+ MagicNumber:Colors.kt$0xff333333
+ MagicNumber:Colors.kt$0xff434343
+ MagicNumber:Colors.kt$0xff696666
+ MagicNumber:Colors.kt$0xff777979
+ MagicNumber:Colors.kt$0xff9f9f9f
+ MagicNumber:Colors.kt$0xffaae6e1
+ MagicNumber:Colors.kt$0xffe3e3e4
+ MagicNumber:Colors.kt$0xfffffbf0
+ MagicNumber:Conduit.kt$Conduit$100
+ MagicNumber:DrawableUtil.kt$DrawableUtil$100
+ MagicNumber:DrawableUtil.kt$DrawableUtil$40f
+ MagicNumber:DurationExtensions.kt$1e9
+ MagicNumber:EditFolderActivity.kt$EditFolderActivity$0.5
+ MagicNumber:ExpandableSpaceList.kt$180
+ MagicNumber:FullscreenDimmingOverlay.kt$FullScreenCreateGroupDimmingOverlay$200
+ MagicNumber:FullscreenDimmingOverlay.kt$FullScreenDimmingOverlay$200
+ MagicNumber:GDriveConduit.kt$GDriveConduit$262144
+ MagicNumber:GDriveConduit.kt$GDriveConduit.Companion$1000
+ MagicNumber:GDriveConduit.kt$GDriveConduit.Companion$20
+ MagicNumber:GDriveConduit.kt$GDriveConduit.Companion$200
+ MagicNumber:GDriveConduit.kt$GDriveConduit.Companion$443
+ MagicNumber:GDriveConduit.kt$GDriveConduit.Companion$80
+ MagicNumber:GDriveConduit.kt$GDriveConduit.Companion$8192
+ MagicNumber:Hbks.kt$Hbks$12
+ MagicNumber:Hbks.kt$Hbks$128
+ MagicNumber:Hbks.kt$Hbks$60
+ MagicNumber:IaConduit.kt$IaConduit$4
+ MagicNumber:InternetArchiveLoginScreen.kt$3000
+ MagicNumber:MainActivity.kt$MainActivity$0.3f
+ MagicNumber:MainActivity.kt$MainActivity$0.75
+ MagicNumber:MainActivity.kt$MainActivity$200
+ MagicNumber:MainActivity.kt$MainActivity$60
+ MagicNumber:MainActivity.kt$MainActivity$8f
+ MagicNumber:MainDrawerContent.kt$0.65f
+ MagicNumber:MainDrawerContent.kt$0.7f
+ MagicNumber:MainDrawerContent.kt$8f
+ MagicNumber:MainMediaScreen.kt$4
+ MagicNumber:MainMediaViewHolder.kt$MainMediaViewHolder$0.5f
+ MagicNumber:MainMediaViewHolder.kt$MainMediaViewHolder$1000
+ MagicNumber:MainMediaViewHolder.kt$MainMediaViewHolder$300
+ MagicNumber:MainMediaViewHolder.kt$MainMediaViewHolder$30f
+ MagicNumber:MainMediaViewHolder.kt$MainMediaViewHolder$5f
+ MagicNumber:Media.kt$Media.Status.DeleteRemote$7
+ MagicNumber:Media.kt$Media.Status.Error$9
+ MagicNumber:Media.kt$Media.Status.Published$3
+ MagicNumber:Media.kt$Media.Status.Uploaded$5
+ MagicNumber:Media.kt$Media.Status.Uploading$4
+ MagicNumber:MediaViewHolder.kt$MediaViewHolder$0.5f
+ MagicNumber:MediaViewHolder.kt$MediaViewHolder$30f
+ MagicNumber:MediaViewHolder.kt$MediaViewHolder$5f
+ MagicNumber:NumericKeypad.kt$3
+ MagicNumber:Onboarding23Activity.kt$Onboarding23Activity$0xffffff
+ MagicNumber:Onboarding23Activity.kt$Onboarding23Activity$2000
+ MagicNumber:Onboarding23Activity.kt$Onboarding23Activity$25F
+ MagicNumber:Onboarding23Activity.kt$Onboarding23Activity$3000
+ MagicNumber:Onboarding23Activity.kt$Onboarding23Activity$999999
+ MagicNumber:Onboarding23FragmentStateAdapter.kt$Onboarding23FragmentStateAdapter$3
+ MagicNumber:Onboarding23InstructionsActivity.kt$Onboarding23InstructionsActivity$200L
+ MagicNumber:Onboarding23InstructionsActivity.kt$Onboarding23InstructionsActivity$3
+ MagicNumber:PasscodeDots.kt$10f
+ MagicNumber:PasscodeDots.kt$15f
+ MagicNumber:PasscodeDots.kt$25f
+ MagicNumber:PasscodeEntryViewModel.kt$PasscodeEntryViewModel$200
+ MagicNumber:PasscodeEntryViewModel.kt$PasscodeEntryViewModel$500
+ MagicNumber:PasscodeSetupViewModel.kt$PasscodeSetupViewModel$100
+ MagicNumber:PasscodeSetupViewModel.kt$PasscodeSetupViewModel$500
+ MagicNumber:Picker.kt$Picker$99
+ MagicNumber:PreviewViewHolder.kt$PreviewViewHolder$0.5f
+ MagicNumber:PreviewViewHolder.kt$PreviewViewHolder$30f
+ MagicNumber:PreviewViewHolder.kt$PreviewViewHolder$5f
+ MagicNumber:PrimaryButton.kt$8f
+ MagicNumber:ProcessingTracker.kt$ProcessingTracker$3
+ MagicNumber:RestEndpointTask.kt$RestEndpointTask$9050
+ MagicNumber:RetrofitModule.kt$60
+ MagicNumber:SaveClient.kt$SaveClient$40L
+ MagicNumber:SnowbirdFileListAdapter.kt$SnowbirdFileListAdapter$40
+ MagicNumber:SnowbirdFileViewModel.kt$SnowbirdFileViewModel$30_000
+ MagicNumber:SnowbirdFileViewModel.kt$SnowbirdFileViewModel$60_000
+ MagicNumber:SnowbirdGroup.kt$10
+ MagicNumber:SnowbirdGroupListAdapter.kt$SnowbirdGroupsAdapter.ViewHolder$40
+ MagicNumber:SnowbirdGroupRepository.kt$SnowbirdGroupRepository$1000
+ MagicNumber:SnowbirdGroupRepository.kt$SnowbirdGroupRepository$5
+ MagicNumber:SnowbirdGroupRepository.kt$SnowbirdGroupRepository$60
+ MagicNumber:SnowbirdGroupViewModel.kt$SnowbirdGroupViewModel$30_000
+ MagicNumber:SnowbirdGroupViewModel.kt$SnowbirdGroupViewModel$60_000
+ MagicNumber:SnowbirdRepo.kt$10
+ MagicNumber:SnowbirdRepoListAdapter.kt$SnowbirdRepoListAdapter.SnowbirdRepoListViewHolder$40
+ MagicNumber:SnowbirdRepoViewModel.kt$SnowbirdRepoViewModel$30_000
+ MagicNumber:SnowbirdRepoViewModel.kt$SnowbirdRepoViewModel$60_000
+ MagicNumber:SnowbirdServiceStatus.kt$SnowbirdServiceStatus$3
+ MagicNumber:SnowbirdServiceStatus.kt$SnowbirdServiceStatus$4
+ MagicNumber:SnowbirdServiceStatus.kt$SnowbirdServiceStatus$5
+ MagicNumber:SnowbirdServiceStatus.kt$SnowbirdServiceStatus$6
+ MagicNumber:SnowbirdServiceStatus.kt$SnowbirdServiceStatus.Companion$3
+ MagicNumber:SnowbirdServiceStatus.kt$SnowbirdServiceStatus.Companion$4
+ MagicNumber:SnowbirdServiceStatus.kt$SnowbirdServiceStatus.Companion$5
+ MagicNumber:SnowbirdServiceStatus.kt$SnowbirdServiceStatus.Companion$6
+ MagicNumber:Space.kt$Space.Type.GDRIVE$4
+ MagicNumber:Space.kt$Space.Type.RAVEN$5
+ MagicNumber:SpaceAdapter.kt$SpaceAdapter.ViewHolder$32
+ MagicNumber:SpaceDrawerAdapter.kt$SpaceDrawerAdapter.SpaceViewHolder$21
+ MagicNumber:SwipeToDeleteCallback.kt$SwipeToDeleteCallback$0.75
+ MagicNumber:TwoLetterDrawable.kt$TwoLetterDrawable$0.5f
+ MagicNumber:TwoLetterDrawable.kt$TwoLetterDrawable$0.8f
+ MagicNumber:UnixSocketClient.kt$UnixSocketClient$200
+ MagicNumber:UnixSocketClient.kt$UnixSocketClient$299
+ MagicNumber:UnixSocketClientFileExtensions.kt$200
+ MagicNumber:UnixSocketClientFileExtensions.kt$299
+ MagicNumber:UnixSocketClientUtilityExtensions.kt$16
+ MagicNumber:UnixSocketClientUtilityExtensions.kt$8192
+ MagicNumber:UploadService.kt$UploadService$7918
+ MagicNumber:Utility.kt$Utility$1024
+ MagicNumber:Utility.kt$Utility$4
+ MagicNumber:VideoRequestHandler.kt$VideoRequestHandler$6
+ MagicNumber:WebDavFragment.kt$WebDavFragment.<no name provided>$200
+ MagicNumber:WebDavFragment.kt$WebDavFragment.<no name provided>$204
+ MatchingDeclarationName:DefaultScaffold.kt$MessageManager
+ MatchingDeclarationName:MainMediaScreen.kt$CollectionSection
+ MatchingDeclarationName:SnowbirdGroupListAdapter.kt$SnowbirdGroupsAdapter : ListAdapter
+ MatchingDeclarationName:TextView.kt$Position
+ MaxLineLength:BiometricAuthenticator.kt$BiometricAuthenticator$return config.biometricAuthEnabled && biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
+ MaxLineLength:BottomSheetExtensions.kt$val bottomSheet: FrameLayout = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) ?: return@setOnShowListener
+ MaxLineLength:BrowseFoldersAdapter.kt$BrowseFoldersAdapter.FolderViewHolder$inner
+ MaxLineLength:ConsentActivity.kt$ConsentActivity$R.string.by_allowing_health_checks_you_give_permission_for_the_app_to_securely_send_health_check_data_to_the_s_team
+ MaxLineLength:CreativeCommonsLicenseManager.kt$CreativeCommonsLicenseManager$swRequireShareAlike.isChecked = isActive && binding.swAllowRemix.isChecked && currentLicense?.contains("-sa", true) ?: false
+ MaxLineLength:DriveServiceHelper.kt$DriveServiceHelper$suspend
+ MaxLineLength:DurationExtensions.kt$*
+ MaxLineLength:GDriveConduit.kt$GDriveConduit.Companion$"mimeType='application/vnd.google-apps.folder' and 'root' in parents and trashed = false"
+ MaxLineLength:GDriveConduit.kt$GDriveConduit.Companion$"mimeType='application/vnd.google-apps.folder' and name = '$folderName' and trashed = false and '$parentId' in parents"
+ MaxLineLength:Hbks.kt$Hbks$}
+ MaxLineLength:IaConduit.kt$IaConduit$// TODO this should make sure we aren't accidentally using one of archive.org's metadata fields by accident
+ MaxLineLength:InternetArchiveDetailsScreen.kt$message = UiText.StringResource(R.string.are_you_sure_you_want_to_remove_this_server_from_the_app)
+ MaxLineLength:InternetArchiveFragment.kt$InternetArchiveFragment$val
+ MaxLineLength:MediaAdapter.kt$MediaAdapter$// CleanInsightsManager.measureEvent("backend", "upload-error", media[pos].space?.friendlyName)
+ MaxLineLength:PasscodeSetupScreen.kt$text = "Make sure you remember this pin. If you forget it, you will need to reset the app, and all data will be erased."
+ MaxLineLength:Prefs.kt$Prefs$get() = prefs?.getString(ProofModeConstants.PREFS_KEY_PASSPHRASE, null) ?: ProofModeConstants.PREFS_KEY_PASSPHRASE_DEFAULT
+ MaxLineLength:ProofModeSettingsActivity.kt$ProofModeSettingsActivity.Fragment$if
+ MaxLineLength:ReviewActivity.kt$ReviewActivity.Companion$fun
+ MaxLineLength:SnowbirdCreateGroupFragment.kt$SnowbirdCreateGroupFragment$SnowbirdCreateGroupFragmentDirections
+ MaxLineLength:SnowbirdFileListFragment.kt$SnowbirdFileListFragment$// viewBinding.snowbirdMediaRecyclerView.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
+ MaxLineLength:SnowbirdFileRepository.kt$SnowbirdFileRepository$private suspend
+ MaxLineLength:SnowbirdFragment.kt$SnowbirdFragment$"save+dweb::?dht=82fd345d484393a96b6e0c5d5e17a85a61c9184cc5a3311ab069d6efa0bf1410&enc=6fa27396fe298f92c91013ac54d8f316c2d45dc3bed0edec73078040aa10feed&pk=f4b404d294817cf11ea7f8ef7231626e03b74f6fafe3271b53918608afa82d12&sk=5482a8f490081be684fbadb8bde7f0a99bab8acdcf1ec094826f0f18e327e399"
+ MaxLineLength:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment$// findNavController().navigate(SnowbirdGroupListFragmentDirections.navigateToSnowbirdShareScreen(groupKey))
+ MaxLineLength:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment$val
+ MaxLineLength:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment.<no name provided>$SnowbirdGroupListFragmentDirections.actionFragmentSnowbirdGroupListToFragmentSnowbirdCreateGroup()
+ MaxLineLength:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment$// findNavController().navigate(SnowbirdRepoListFragmentDirections.navigateToSnowbirdListFilesScreen(groupKey, repoKey))
+ MaxLineLength:SpaceAdapter.kt$SpaceAdapter$class
+ MaxLineLength:SpaceDrawerAdapter.kt$SpaceDrawerAdapter$class
+ MaxLineLength:SpaceDrawerAdapter.kt$SpaceDrawerAdapter$val previousIndex = currentList.indexOfFirst { it is SpaceItem.SpaceItemData && it.space.id == selectedSpace?.id }
+ MaxLineLength:SpaceDrawerAdapter.kt$SpaceDrawerAdapter.Companion.<no name provided>$oldItem is SpaceItem.SpaceItemData && newItem is SpaceItem.SpaceItemData -> oldItem.space.friendlyName == newItem.space.friendlyName
+ MaxLineLength:SpaceDrawerAdapter.kt$SpaceDrawerAdapter.Companion.<no name provided>$oldItem is SpaceItem.SpaceItemData && newItem is SpaceItem.SpaceItemData -> oldItem.space.id == newItem.space.id
+ MaxLineLength:SpaceDrawerAdapter.kt$SpaceDrawerAdapter.SpaceViewHolder$val previousIndex = currentList.indexOfFirst { it is SpaceItem.SpaceItemData && it.space.id == selectedSpace?.id }
+ MaxLineLength:Utility.kt$Utility$fun
+ MaxLineLength:WebDavSetupLicenseFragment.kt$WebDavSetupLicenseFragment$val
+ MaximumLineLength:BiometricAuthenticator.kt$BiometricAuthenticator$
+ MaximumLineLength:BottomSheetExtensions.kt$
+ MaximumLineLength:BrowseFoldersAdapter.kt$BrowseFoldersAdapter$
+ MaximumLineLength:ConsentActivity.kt$ConsentActivity$
+ MaximumLineLength:CreativeCommonsLicenseManager.kt$CreativeCommonsLicenseManager$
+ MaximumLineLength:DriveServiceHelper.kt$DriveServiceHelper$
+ MaximumLineLength:GDriveConduit.kt$GDriveConduit.Companion$
+ MaximumLineLength:Hbks.kt$Hbks$
+ MaximumLineLength:InternetArchiveDetailsScreen.kt$
+ MaximumLineLength:InternetArchiveFragment.kt$InternetArchiveFragment$
+ MaximumLineLength:PasscodeSetupScreen.kt$
+ MaximumLineLength:Prefs.kt$Prefs$
+ MaximumLineLength:ProofModeSettingsActivity.kt$ProofModeSettingsActivity.Fragment$
+ MaximumLineLength:ReviewActivity.kt$ReviewActivity.Companion$
+ MaximumLineLength:SnowbirdCreateGroupFragment.kt$SnowbirdCreateGroupFragment$
+ MaximumLineLength:SnowbirdFileRepository.kt$SnowbirdFileRepository$
+ MaximumLineLength:SnowbirdFragment.kt$SnowbirdFragment$
+ MaximumLineLength:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment$
+ MaximumLineLength:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment.<no name provided>$
+ MaximumLineLength:SpaceAdapter.kt$SpaceAdapter$class
+ MaximumLineLength:SpaceDrawerAdapter.kt$SpaceDrawerAdapter$
+ MaximumLineLength:SpaceDrawerAdapter.kt$SpaceDrawerAdapter$class
+ MaximumLineLength:SpaceDrawerAdapter.kt$SpaceDrawerAdapter.Companion.<no name provided>$
+ MaximumLineLength:SpaceDrawerAdapter.kt$SpaceDrawerAdapter.SpaceViewHolder$
+ MaximumLineLength:Utility.kt$Utility$
+ MaximumLineLength:WebDavSetupLicenseFragment.kt$WebDavSetupLicenseFragment$
+ MemberNameEqualsClassName:Prefs.kt$Prefs$private var prefs: SharedPreferences? = null
+ ModifierClickableOrder:NumericKeypad.kt$clickable( interactionSource = interactionSource, indication = null, enabled = enabled, onClick = { hapticManager.performHapticFeedback(AppHapticFeedbackType.KeyPress) onClick() } )
+ ModifierMissing:AddFolderScreen.kt$AddFolderScreenContent
+ ModifierMissing:AddFolderScreen.kt$FolderOption
+ ModifierMissing:BrowseFolderScreen.kt$BrowseFolderItem
+ ModifierMissing:BrowseFolderScreen.kt$BrowseFolderScreenContent
+ ModifierMissing:ExpandableSpaceList.kt$DrawerSpaceListItem
+ ModifierMissing:ExpandableSpaceList.kt$ExpandableSpaceList
+ ModifierMissing:FolderOptionsPopup.kt$FolderOptionsPopup
+ ModifierMissing:HomeAppBar.kt$HomeAppBar
+ ModifierMissing:HomeScreen.kt$HomeScreenContent
+ ModifierMissing:InternetArchiveLoginScreen.kt$ComposeAppBar
+ ModifierMissing:MainBottomBar.kt$BottomNavMenuItem
+ ModifierMissing:MainBottomBar.kt$MainBottomBar
+ ModifierMissing:MainDrawerContent.kt$MainDrawerContent
+ ModifierMissing:MainDrawerContent.kt$MainDrawerFolderListItem
+ ModifierMissing:MainMediaScreen.kt$CollectionHeaderView
+ ModifierMissing:MainMediaScreen.kt$CollectionSectionView
+ ModifierMissing:MainMediaScreen.kt$ErrorIndicator
+ ModifierMissing:MainMediaScreen.kt$MainMediaScreen
+ ModifierMissing:MainMediaScreen.kt$UploadProgress
+ ModifierMissing:MainMediaScreen.kt$WelcomeMessage
+ ModifierMissing:MediaCacheScreen.kt$CacheFileItem
+ ModifierMissing:MediaCacheScreen.kt$MediaCacheScreen
+ ModifierMissing:NumericKeypad.kt$NumericKeypad
+ ModifierMissing:PasscodeDots.kt$PasscodeDots
+ ModifierMissing:PasscodeEntryScreen.kt$PasscodeEntryScreenContent
+ ModifierMissing:Preview.kt$DefaultBoxPreview
+ ModifierMissing:Preview.kt$DefaultEmptyScaffoldPreview
+ ModifierMissing:Preview.kt$DefaultScaffoldPreview
+ ModifierMissing:ProofModeScreen.kt$ProofModeScreenContent
+ ModifierMissing:ServerOptionItem.kt$ServerOptionItem
+ ModifierMissing:SettingsScreen.kt$SettingsScreen
+ ModifierMissing:SpaceListScreen.kt$SpaceListItem
+ ModifierMissing:SpaceListScreen.kt$SpaceListScreen
+ ModifierMissing:SpaceListScreen.kt$SpaceListScreenContent
+ ModifierMissing:SpaceSetupScreen.kt$SpaceSetupScreen
+ MultiLineIfElse:EditFolderActivity.kt$EditFolderActivity$R.string.action_archive_project
+ MultiLineIfElse:EditFolderActivity.kt$EditFolderActivity$R.string.action_unarchive_project
+ MultiLineIfElse:FileUtils.kt$FileUtils$Log.d( "$TAG File -", "Authority: " + uri.authority + ", Fragment: " + uri.fragment + ", Port: " + uri.port + ", Query: " + uri.query + ", Scheme: " + uri.scheme + ", Host: " + uri.host + ", Segments: " + uri.pathSegments.toString() )
+ MultiLineIfElse:MainActivity.kt$MainActivity$false
+ MultiLineIfElse:MediaViewHolder.kt$MediaViewHolder$R.drawable.ic_edit_selected
+ MultiLineIfElse:MediaViewHolder.kt$MediaViewHolder$R.drawable.ic_edit_unselected
+ MultiLineIfElse:MediaViewHolder.kt$MediaViewHolder$R.drawable.ic_flag_selected
+ MultiLineIfElse:MediaViewHolder.kt$MediaViewHolder$R.drawable.ic_flag_unselected
+ MultiLineIfElse:MediaViewHolder.kt$MediaViewHolder$R.drawable.ic_location_selected
+ MultiLineIfElse:MediaViewHolder.kt$MediaViewHolder$R.drawable.ic_location_unselected
+ MultiLineIfElse:MediaViewHolder.kt$MediaViewHolder$R.drawable.ic_tag_selected
+ MultiLineIfElse:MediaViewHolder.kt$MediaViewHolder$R.drawable.ic_tag_unselected
+ MultiLineIfElse:PasscodeDots.kt$MaterialTheme.colorScheme.onBackground
+ MultiLineIfElse:PasscodeDots.kt$MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
+ MultiLineIfElse:PasscodeEntryViewModel.kt$PasscodeEntryViewModel$null
+ MultiLineIfElse:PasscodeEntryViewModel.kt$PasscodeEntryViewModel$state
+ MultiLineIfElse:PasscodeEntryViewModel.kt$PasscodeEntryViewModel$state.copy(passcode = state.passcode + number)
+ MultiLineIfElse:PasscodeEntryViewModel.kt$PasscodeEntryViewModel$state.copy(passcode = state.passcode.dropLast(1))
+ MultiLineIfElse:PasscodeSetupViewModel.kt$PasscodeSetupViewModel$state
+ MultiLineIfElse:PasscodeSetupViewModel.kt$PasscodeSetupViewModel$state.copy(passcode = state.passcode + number)
+ MultiLineIfElse:PasscodeSetupViewModel.kt$PasscodeSetupViewModel$state.copy(passcode = state.passcode.dropLast(1))
+ MultiLineIfElse:RequestBodyUtil.kt$RequestBodyUtil.<no name provided>$FileInputStream( uri.path?.let { File(it) } )
+ MultiLineIfElse:RequestBodyUtil.kt$RequestBodyUtil.<no name provided>$cr.openInputStream(uri)
+ MultiLineIfElse:SwipeToDeleteCallback.kt$SwipeToDeleteCallback$0
+ MultiLineIfElse:SwipeToDeleteCallback.kt$SwipeToDeleteCallback$ColorDrawable(ContextCompat.getColor(context, R.color.colorDanger))
+ MultiLineIfElse:SwipeToDeleteCallback.kt$SwipeToDeleteCallback$ContextCompat.getColor(context, R.color.colorOnBackground)
+ MultiLineIfElse:SwipeToDeleteCallback.kt$SwipeToDeleteCallback$ContextCompat.getDrawable(context, R.drawable.ic_delete)
+ MultiLineIfElse:SwipeToDeleteCallback.kt$SwipeToDeleteCallback$null
+ NestedBlockDepth:MediaViewHolder.kt$MediaViewHolder$@SuppressLint("SetTextI18n") fun bind(media: Media? = null, batchMode: Boolean = false, doImageFade: Boolean = true)
+ NestedBlockDepth:UriExtensions.kt$fun Uri.getFilename(applicationContext: Context): String?
+ NestedBlockDepth:WebDavConduit.kt$WebDavConduit$@Throws(IOException::class) private suspend fun uploadChunked(base: HttpUrl, path: List<String>, fileName: String): Boolean
+ NoBlankLineBeforeRbrace:AddFolderScreen.kt$
+ NoBlankLineBeforeRbrace:BaseDialog.kt$
+ NoBlankLineBeforeRbrace:BrowseFolderScreen.kt$
+ NoBlankLineBeforeRbrace:CreateNewFolderFragment.kt$CreateNewFolderFragment$
+ NoBlankLineBeforeRbrace:ExpandableSpaceList.kt$
+ NoBlankLineBeforeRbrace:FileUtils.kt$FileUtils$
+ NoBlankLineBeforeRbrace:FolderAdapter.kt$FolderAdapter.ViewHolder$
+ NoBlankLineBeforeRbrace:FolderOptionsPopup.kt$
+ NoBlankLineBeforeRbrace:HomeActivity.kt$HomeActivity$
+ NoBlankLineBeforeRbrace:HomeAppBar.kt$
+ NoBlankLineBeforeRbrace:HomeScreen.kt$
+ NoBlankLineBeforeRbrace:HomeScreen.kt$HomeViewModel$
+ NoBlankLineBeforeRbrace:IaConduit.kt$IaConduit.<no name provided>$
+ NoBlankLineBeforeRbrace:InternetArchiveActivity.kt$InternetArchiveActivity$
+ NoBlankLineBeforeRbrace:InternetArchiveDetailsScreen.kt$
+ NoBlankLineBeforeRbrace:InternetArchiveLoginUseCase.kt$InternetArchiveLoginUseCase$
+ NoBlankLineBeforeRbrace:InternetArchiveLoginViewModel.kt$InternetArchiveLoginViewModel$
+ NoBlankLineBeforeRbrace:MainActivity.kt$MainActivity$
+ NoBlankLineBeforeRbrace:MainBottomBar.kt$
+ NoBlankLineBeforeRbrace:MainDrawerContent.kt$
+ NoBlankLineBeforeRbrace:MainMediaViewHolder.kt$MainMediaViewHolder$
+ NoBlankLineBeforeRbrace:MainMediaViewModel.kt$MainMediaViewModel$
+ NoBlankLineBeforeRbrace:MediaCacheScreen.kt$
+ NoBlankLineBeforeRbrace:NumericKeypad.kt$
+ NoBlankLineBeforeRbrace:Onboarding23InstructionsActivity.kt$Onboarding23InstructionsActivity.<no name provided>$
+ NoBlankLineBeforeRbrace:PackageManager.kt$
+ NoBlankLineBeforeRbrace:PasscodeEntryScreen.kt$
+ NoBlankLineBeforeRbrace:PasscodeEntryViewModel.kt$PasscodeEntryViewModel$
+ NoBlankLineBeforeRbrace:Preview.kt$
+ NoBlankLineBeforeRbrace:PreviewActivity.kt$PreviewActivity$
+ NoBlankLineBeforeRbrace:PreviewViewHolder.kt$PreviewViewHolder$
+ NoBlankLineBeforeRbrace:Project.kt$Project$
+ NoBlankLineBeforeRbrace:ProofModeSettingsActivity.kt$ProofModeSettingsActivity$
+ NoBlankLineBeforeRbrace:RetrofitAPI.kt$RetrofitAPI$
+ NoBlankLineBeforeRbrace:SectionViewHolder.kt$SectionViewHolder.Companion$
+ NoBlankLineBeforeRbrace:ServerOptionItem.kt$
+ NoBlankLineBeforeRbrace:SmartFragmentStatePagerAdapter.kt$SmartFragmentStatePagerAdapter$
+ NoBlankLineBeforeRbrace:SnowbirdCreateGroupFragment.kt$SnowbirdCreateGroupFragment$
+ NoBlankLineBeforeRbrace:SnowbirdFileRepository.kt$SnowbirdFileRepository$
+ NoBlankLineBeforeRbrace:Space.kt$Space$
+ NoBlankLineBeforeRbrace:SpaceAdapter.kt$SpaceAdapter.Companion$
+ NoBlankLineBeforeRbrace:SpaceListFragment.kt$SpaceListFragment$
+ NoBlankLineBeforeRbrace:SpaceListScreen.kt$
+ NoBlankLineBeforeRbrace:SpaceSetupActivity.kt$SpaceSetupActivity$
+ NoBlankLineBeforeRbrace:SpaceSetupFragment.kt$SpaceSetupFragment$
+ NoBlankLineBeforeRbrace:TorStatusContentProvider.kt$TorStatusContentProvider$
+ NoBlankLineBeforeRbrace:UiImage.kt$UiImage$
+ NoBlankLineBeforeRbrace:WebDavFragment.kt$WebDavFragment$
+ NoConsecutiveBlankLines:AddFolderActivity.kt$AddFolderActivity$
+ NoConsecutiveBlankLines:AddFolderScreen.kt$
+ NoConsecutiveBlankLines:AppLogger.kt$
+ NoConsecutiveBlankLines:BadgeDrawable.kt$BadgeDrawable$
+ NoConsecutiveBlankLines:BaseButton.kt$
+ NoConsecutiveBlankLines:BaseComposeActivity.kt$BaseComposeActivity$
+ NoConsecutiveBlankLines:BaseDialog.kt$
+ NoConsecutiveBlankLines:BrowseFolderScreen.kt$
+ NoConsecutiveBlankLines:BrowseFoldersAdapter.kt$BrowseFoldersAdapter.FolderViewHolder$
+ NoConsecutiveBlankLines:BrowseFoldersFragment.kt$
+ NoConsecutiveBlankLines:BrowseFoldersFragment.kt$BrowseFoldersFragment$
+ NoConsecutiveBlankLines:BrowseFoldersViewModel.kt$
+ NoConsecutiveBlankLines:Collection.kt$Collection$
+ NoConsecutiveBlankLines:ContentPickerFragment.kt$ContentPickerFragment$
+ NoConsecutiveBlankLines:CoreModule.kt$
+ NoConsecutiveBlankLines:CustomBottomNavBar.kt$CustomBottomNavBar$
+ NoConsecutiveBlankLines:DialogConfigBuilder.kt$
+ NoConsecutiveBlankLines:EditFolderActivity.kt$EditFolderActivity$
+ NoConsecutiveBlankLines:Effects.kt$
+ NoConsecutiveBlankLines:FeaturesModule.kt$
+ NoConsecutiveBlankLines:FileUtils.kt$FileUtils$
+ NoConsecutiveBlankLines:FolderAdapter.kt$FolderAdapter.ViewHolder$
+ NoConsecutiveBlankLines:FolderDrawerAdapter.kt$
+ NoConsecutiveBlankLines:FolderDrawerAdapter.kt$FolderDrawerAdapter$
+ NoConsecutiveBlankLines:FoldersActivity.kt$FoldersActivity$
+ NoConsecutiveBlankLines:GeneralSettingsActivity.kt$
+ NoConsecutiveBlankLines:GeneralSettingsActivity.kt$GeneralSettingsActivity$
+ NoConsecutiveBlankLines:GeneralSettingsActivity.kt$GeneralSettingsActivity.Fragment$
+ NoConsecutiveBlankLines:Hbks.kt$Hbks$
+ NoConsecutiveBlankLines:HomeScreen.kt$
+ NoConsecutiveBlankLines:IaConduit.kt$IaConduit$
+ NoConsecutiveBlankLines:InternetArchive.kt$InternetArchive$
+ NoConsecutiveBlankLines:InternetArchiveActivity.kt$InternetArchiveActivity$
+ NoConsecutiveBlankLines:InternetArchiveDetailsScreen.kt$
+ NoConsecutiveBlankLines:InternetArchiveDetailsState.kt$
+ NoConsecutiveBlankLines:MainActivity.kt$
+ NoConsecutiveBlankLines:MainActivity.kt$MainActivity$
+ NoConsecutiveBlankLines:MainDrawerContent.kt$
+ NoConsecutiveBlankLines:MainMediaAdapter.kt$MainMediaAdapter$
+ NoConsecutiveBlankLines:MainMediaAdapterTest.kt$
+ NoConsecutiveBlankLines:MainMediaAdapterTest.kt$MainMediaAdapterTest$
+ NoConsecutiveBlankLines:MainMediaFragment.kt$MainMediaFragment$
+ NoConsecutiveBlankLines:MainMediaScreen.kt$
+ NoConsecutiveBlankLines:MainMediaViewHolder.kt$MainMediaViewHolder$
+ NoConsecutiveBlankLines:MainMediaViewModel.kt$
+ NoConsecutiveBlankLines:MainViewModel.kt$MainViewModel$
+ NoConsecutiveBlankLines:Media.kt$Media$
+ NoConsecutiveBlankLines:Media.kt$Media.Companion$
+ NoConsecutiveBlankLines:MediaAdapter.kt$MediaAdapter$
+ NoConsecutiveBlankLines:MediaCacheScreen.kt$
+ NoConsecutiveBlankLines:MediaViewHolder.kt$MediaViewHolder$
+ NoConsecutiveBlankLines:Notifier.kt$
+ NoConsecutiveBlankLines:NumericKeypad.kt$
+ NoConsecutiveBlankLines:Onboarding23SlideFragment.kt$Onboarding23SlideFragment$
+ NoConsecutiveBlankLines:PasscodeEntryActivity.kt$PasscodeEntryActivity$
+ NoConsecutiveBlankLines:PasscodeEntryScreen.kt$
+ NoConsecutiveBlankLines:PasscodeSetupActivity.kt$PasscodeSetupActivity$
+ NoConsecutiveBlankLines:PasscodeSetupScreen.kt$
+ NoConsecutiveBlankLines:PreviewActivity.kt$PreviewActivity$
+ NoConsecutiveBlankLines:PreviewViewHolder.kt$PreviewViewHolder$
+ NoConsecutiveBlankLines:ProofModeScreen.kt$
+ NoConsecutiveBlankLines:ProofModeSettingsActivity.kt$
+ NoConsecutiveBlankLines:ProofModeSettingsActivity.kt$ProofModeSettingsActivity$
+ NoConsecutiveBlankLines:ProofModeSettingsActivity.kt$ProofModeSettingsActivity.Fragment$
+ NoConsecutiveBlankLines:ReviewActivity.kt$ReviewActivity$
+ NoConsecutiveBlankLines:SettingsFragment.kt$SettingsFragment$
+ NoConsecutiveBlankLines:SettingsScreen.kt$
+ NoConsecutiveBlankLines:SnowbirdCreateGroupFragment.kt$SnowbirdCreateGroupFragment$
+ NoConsecutiveBlankLines:SnowbirdFileItem.kt$
+ NoConsecutiveBlankLines:SnowbirdJoinGroupFragment.kt$SnowbirdJoinGroupFragment.Companion$
+ NoConsecutiveBlankLines:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment$
+ NoConsecutiveBlankLines:SpaceAdapter.kt$SpaceAdapter.ViewHolder$
+ NoConsecutiveBlankLines:SpaceDrawerAdapter.kt$SpaceDrawerAdapter$
+ NoConsecutiveBlankLines:SpaceListFragment.kt$SpaceListFragment$
+ NoConsecutiveBlankLines:SpaceListScreen.kt$
+ NoConsecutiveBlankLines:SpaceSetupActivity.kt$SpaceSetupActivity$
+ NoConsecutiveBlankLines:SpaceSetupScreen.kt$
+ NoConsecutiveBlankLines:Theme.kt$
+ NoConsecutiveBlankLines:UiImage.kt$
+ NoConsecutiveBlankLines:UiImage.kt$UiImage$
+ NoConsecutiveBlankLines:View.kt$
+ NoConsecutiveBlankLines:ViewExtension.kt$
+ NoConsecutiveBlankLines:WebDavConduit.kt$
+ NoConsecutiveBlankLines:WebDavFragment.kt$WebDavFragment$
+ NoConsecutiveBlankLines:WebDavSetupLicenseFragment.kt$WebDavSetupLicenseFragment$
+ NoEmptyClassBody:MainMediaViewModel.kt$MainMediaViewModel${ }
+ NoEmptyFirstLineInMethodBlock:AddFolderActivity.kt$AddFolderActivity$
+ NoEmptyFirstLineInMethodBlock:AddFolderScreen.kt$
+ NoEmptyFirstLineInMethodBlock:AppLogger.kt$AppLogger$
+ NoEmptyFirstLineInMethodBlock:BaseButton.kt$
+ NoEmptyFirstLineInMethodBlock:BaseDialog.kt$
+ NoEmptyFirstLineInMethodBlock:BrowseFolderScreen.kt$
+ NoEmptyFirstLineInMethodBlock:BrowseFoldersAdapter.kt$BrowseFoldersAdapter.FolderViewHolder$
+ NoEmptyFirstLineInMethodBlock:CreateNewFolderFragment.kt$CreateNewFolderFragment$
+ NoEmptyFirstLineInMethodBlock:DefaultScaffold.kt$
+ NoEmptyFirstLineInMethodBlock:DialogConfigBuilder.kt$DialogBuilder$
+ NoEmptyFirstLineInMethodBlock:ExpandableSpaceList.kt$
+ NoEmptyFirstLineInMethodBlock:FileUtils.kt$FileUtils$
+ NoEmptyFirstLineInMethodBlock:FolderAdapter.kt$FolderAdapter.ViewHolder$
+ NoEmptyFirstLineInMethodBlock:FolderDrawerAdapter.kt$FolderDrawerAdapter.FolderViewHolder$
+ NoEmptyFirstLineInMethodBlock:FolderOptionsPopup.kt$
+ NoEmptyFirstLineInMethodBlock:FoldersActivity.kt$FoldersActivity$
+ NoEmptyFirstLineInMethodBlock:HomeAppBar.kt$
+ NoEmptyFirstLineInMethodBlock:HomeScreen.kt$
+ NoEmptyFirstLineInMethodBlock:InternetArchiveActivity.kt$InternetArchiveActivity$
+ NoEmptyFirstLineInMethodBlock:InternetArchiveDetailsScreen.kt$
+ NoEmptyFirstLineInMethodBlock:InternetArchiveFragment.kt$InternetArchiveFragment$
+ NoEmptyFirstLineInMethodBlock:InternetArchiveLoginScreen.kt$
+ NoEmptyFirstLineInMethodBlock:MainActivity.kt$MainActivity$
+ NoEmptyFirstLineInMethodBlock:MainBottomBar.kt$
+ NoEmptyFirstLineInMethodBlock:MainDrawerContent.kt$
+ NoEmptyFirstLineInMethodBlock:MainMediaViewHolder.kt$MainMediaViewHolder$
+ NoEmptyFirstLineInMethodBlock:NumericKeypad.kt$
+ NoEmptyFirstLineInMethodBlock:PasscodeDots.kt$
+ NoEmptyFirstLineInMethodBlock:PasscodeEntryScreen.kt$
+ NoEmptyFirstLineInMethodBlock:PasscodeEntryViewModel.kt$PasscodeEntryViewModel$
+ NoEmptyFirstLineInMethodBlock:PasscodeSetupScreen.kt$
+ NoEmptyFirstLineInMethodBlock:PasscodeSetupViewModel.kt$PasscodeSetupViewModel$
+ NoEmptyFirstLineInMethodBlock:Picker.kt$Picker$
+ NoEmptyFirstLineInMethodBlock:Preview.kt$
+ NoEmptyFirstLineInMethodBlock:PreviewActivity.kt$PreviewActivity$
+ NoEmptyFirstLineInMethodBlock:PreviewViewHolder.kt$PreviewViewHolder$
+ NoEmptyFirstLineInMethodBlock:PrimaryButton.kt$
+ NoEmptyFirstLineInMethodBlock:ProofModeScreen.kt$
+ NoEmptyFirstLineInMethodBlock:ProofModeSettingsActivity.kt$ProofModeSettingsActivity.Fragment$
+ NoEmptyFirstLineInMethodBlock:SaveClient.kt$SaveClient.Companion$
+ NoEmptyFirstLineInMethodBlock:ServerOptionItem.kt$
+ NoEmptyFirstLineInMethodBlock:SettingsScreen.kt$
+ NoEmptyFirstLineInMethodBlock:SnowbirdFragment.kt$SnowbirdFragment$
+ NoEmptyFirstLineInMethodBlock:SnowbirdGroupListAdapter.kt$SnowbirdGroupsAdapter.ViewHolder$
+ NoEmptyFirstLineInMethodBlock:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment$
+ NoEmptyFirstLineInMethodBlock:Space.kt$Space$
+ NoEmptyFirstLineInMethodBlock:SpaceAdapter.kt$SpaceAdapter.ViewHolder$
+ NoEmptyFirstLineInMethodBlock:SpaceDrawerAdapter.kt$SpaceDrawerAdapter.SpaceViewHolder$
+ NoEmptyFirstLineInMethodBlock:SpaceListFragment.kt$SpaceListFragment$
+ NoEmptyFirstLineInMethodBlock:SpaceListScreen.kt$
+ NoEmptyFirstLineInMethodBlock:SpaceSetupFragment.kt$SpaceSetupFragment$
+ NoEmptyFirstLineInMethodBlock:UploadService.kt$UploadService$
+ NoEmptyFirstLineInMethodBlock:WebDavFragment.kt$WebDavFragment$
+ NoEmptyFirstLineInMethodBlock:WebDavSetupLicenseFragment.kt$WebDavSetupLicenseFragment$
+ NoMultipleSpaces:CleanInsightsManager.kt$CleanInsightsManager$
+ NoMultipleSpaces:DialogConfigBuilder.kt$
+ NoMultipleSpaces:GDriveConduit.kt$GDriveConduit$
+ NoMultipleSpaces:Media.kt$Media$
+ NoMultipleSpaces:NumericKeypad.kt$
+ NoMultipleSpaces:PreviewAdapter.kt$PreviewAdapter$
+ NoMultipleSpaces:RequestBodyUtil.kt$<no name provided>$
+ NoMultipleSpaces:ScryptHashingStrategy.kt$ScryptHashingStrategy.Companion$
+ NoMultipleSpaces:SnowbirdFragment.kt$SnowbirdFragment$
+ NoMultipleSpaces:SwipeToDeleteCallback.kt$SwipeToDeleteCallback$
+ NoUnusedImports:AddFolderScreen.kt$net.opendasharchive.openarchive.features.folders.AddFolderScreen.kt
+ NoUnusedImports:AppLogger.kt$net.opendasharchive.openarchive.core.logger.AppLogger.kt
+ NoUnusedImports:BaseComposeActivity.kt$net.opendasharchive.openarchive.features.core.BaseComposeActivity.kt
+ NoUnusedImports:BaseDialog.kt$net.opendasharchive.openarchive.features.core.dialog.BaseDialog.kt
+ NoUnusedImports:BottomSheetExtensions.kt$net.opendasharchive.openarchive.extensions.BottomSheetExtensions.kt
+ NoUnusedImports:BrowseFolderScreen.kt$net.opendasharchive.openarchive.features.folders.BrowseFolderScreen.kt
+ NoUnusedImports:BrowseFoldersFragment.kt$net.opendasharchive.openarchive.features.folders.BrowseFoldersFragment.kt
+ NoUnusedImports:CoreModule.kt$net.opendasharchive.openarchive.core.di.CoreModule.kt
+ NoUnusedImports:CreateNewFolderFragment.kt$net.opendasharchive.openarchive.features.folders.CreateNewFolderFragment.kt
+ NoUnusedImports:CustomButton.kt$net.opendasharchive.openarchive.features.main.ui.CustomButton.kt
+ NoUnusedImports:DialogConfigBuilder.kt$net.opendasharchive.openarchive.features.core.dialog.DialogConfigBuilder.kt
+ NoUnusedImports:ExpandableSpaceList.kt$net.opendasharchive.openarchive.features.main.ui.components.ExpandableSpaceList.kt
+ NoUnusedImports:FeaturesModule.kt$net.opendasharchive.openarchive.core.di.FeaturesModule.kt
+ NoUnusedImports:FullscreenDimmingOverlay.kt$net.opendasharchive.openarchive.util.FullscreenDimmingOverlay.kt
+ NoUnusedImports:HomeActivity.kt$net.opendasharchive.openarchive.features.main.HomeActivity.kt
+ NoUnusedImports:InternetArchiveDetailsScreen.kt$net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsScreen.kt
+ NoUnusedImports:InternetArchiveLocalSource.kt$net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveLocalSource.kt
+ NoUnusedImports:InternetArchiveLoginScreen.kt$net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen.kt
+ NoUnusedImports:MainActivity.kt$net.opendasharchive.openarchive.features.main.MainActivity.kt
+ NoUnusedImports:MainMediaScreen.kt$net.opendasharchive.openarchive.features.main.ui.MainMediaScreen.kt
+ NoUnusedImports:MainMediaViewHolder.kt$net.opendasharchive.openarchive.features.main.adapters.MainMediaViewHolder.kt
+ NoUnusedImports:MediaCacheScreen.kt$net.opendasharchive.openarchive.features.main.ui.MediaCacheScreen.kt
+ NoUnusedImports:MediaViewHolder.kt$net.opendasharchive.openarchive.db.MediaViewHolder.kt
+ NoUnusedImports:NumericKeypad.kt$net.opendasharchive.openarchive.features.settings.passcode.components.NumericKeypad.kt
+ NoUnusedImports:PasscodeEntryScreen.kt$net.opendasharchive.openarchive.features.settings.passcode.passcode_entry.PasscodeEntryScreen.kt
+ NoUnusedImports:PasscodeSetupActivity.kt$net.opendasharchive.openarchive.features.settings.passcode.passcode_setup.PasscodeSetupActivity.kt
+ NoUnusedImports:PasscodeSetupScreen.kt$net.opendasharchive.openarchive.features.settings.passcode.passcode_setup.PasscodeSetupScreen.kt
+ NoUnusedImports:ProofModeSettingsActivity.kt$net.opendasharchive.openarchive.features.settings.ProofModeSettingsActivity.kt
+ NoUnusedImports:RequestBodyUtil.kt$net.opendasharchive.openarchive.services.internetarchive.RequestBodyUtil.kt
+ NoUnusedImports:RestEndpointTask.kt$net.opendasharchive.openarchive.features.main.RestEndpointTask.kt
+ NoUnusedImports:ReviewActivity.kt$net.opendasharchive.openarchive.features.media.ReviewActivity.kt
+ NoUnusedImports:SaveApp.kt$net.opendasharchive.openarchive.SaveApp.kt
+ NoUnusedImports:ServerOptionItem.kt$net.opendasharchive.openarchive.features.spaces.ServerOptionItem.kt
+ NoUnusedImports:SettingsFragment.kt$net.opendasharchive.openarchive.features.settings.SettingsFragment.kt
+ NoUnusedImports:SettingsScreen.kt$net.opendasharchive.openarchive.features.settings.SettingsScreen.kt
+ NoUnusedImports:SpaceListFragment.kt$net.opendasharchive.openarchive.features.spaces.SpaceListFragment.kt
+ NoUnusedImports:SpaceListScreen.kt$net.opendasharchive.openarchive.features.spaces.SpaceListScreen.kt
+ NoUnusedImports:SpaceSetupActivity.kt$net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity.kt
+ NoUnusedImports:SpaceSetupFragment.kt$net.opendasharchive.openarchive.features.settings.SpaceSetupFragment.kt
+ NoUnusedImports:UnixSocketClient.kt$net.opendasharchive.openarchive.features.main.UnixSocketClient.kt
+ NoUnusedImports:UploadManagerActivity.kt$net.opendasharchive.openarchive.upload.UploadManagerActivity.kt
+ NoUnusedImports:UploadManagerFragment.kt$net.opendasharchive.openarchive.upload.UploadManagerFragment.kt
+ NoWildcardImports:BadgeDrawable.kt$import android.graphics.*
+ NoWildcardImports:CleanInsightsManager.kt$import org.cleaninsights.sdk.*
+ NoWildcardImports:Hbks.kt$import java.security.*
+ NoWildcardImports:Hbks.kt$import javax.crypto.*
+ NoWildcardImports:IaConduit.kt$import okhttp3.*
+ NoWildcardImports:MediaCacheScreen.kt$import androidx.compose.foundation.layout.*
+ NoWildcardImports:RequestBodyUtil.kt$import java.io.*
+ NoWildcardImports:UploadService.kt$import android.app.*
+ PackageName:PasscodeEntryActivity.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_entry
+ PackageName:PasscodeEntryScreen.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_entry
+ PackageName:PasscodeEntryViewModel.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_entry
+ PackageName:PasscodeSetupActivity.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_setup
+ PackageName:PasscodeSetupScreen.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_setup
+ PackageName:PasscodeSetupViewModel.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_setup
+ PackageNaming:PasscodeEntryActivity.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_entry
+ PackageNaming:PasscodeEntryScreen.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_entry
+ PackageNaming:PasscodeEntryViewModel.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_entry
+ PackageNaming:PasscodeSetupActivity.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_setup
+ PackageNaming:PasscodeSetupScreen.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_setup
+ PackageNaming:PasscodeSetupViewModel.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_setup
+ ParameterListWrapping:AddMediaDialogFragment.kt$AddMediaDialogFragment$( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? )
+ ParameterListWrapping:AlertHelper.kt$AlertHelper.Companion$( context: Context, message: Int?, title: Int? = R.string.error, icon: Int? = null, buttons: List<Button>? = listOf(Button()) )
+ ParameterListWrapping:AlertHelper.kt$AlertHelper.Companion$( context: Context, message: String? = null, title: Int? = R.string.error, icon: Int? = null, buttons: List<Button>? = listOf(Button()) )
+ ParameterListWrapping:BiometricAuthenticator.kt$BiometricAuthenticator$( private val activity: BaseActivity, private val config: AppConfig )
+ ParameterListWrapping:DialogConfigBuilder.kt$( resourceProvider: ResourceProvider = this.requireResourceProvider(), block: DialogBuilder.() -> Unit)
+ ParameterListWrapping:DialogConfigBuilder.kt$(resourceProvider: ResourceProvider = this.requireResourceProvider(), block: DialogBuilder.() -> Unit)
+ ParameterListWrapping:FileUtils.kt$FileUtils$( context: Context, uri: Uri, selection: String?, selectionArgs: Array<String>?)
+ ParameterListWrapping:FileUtils.kt$FileUtils$(context: Context, uri: Uri, selection: String?, selectionArgs: Array<String>?)
+ ParameterListWrapping:GDriveFragment.kt$GDriveFragment$( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? )
+ ParameterListWrapping:Hbks.kt$Hbks$( ciphertext: ByteArray?, key: SecretKey?, activity: FragmentActivity? = null, completed: (plaintext: String?, exception: Exception?) -> Unit)
+ ParameterListWrapping:Hbks.kt$Hbks$( plaintext: String?, key: SecretKey?, activity: FragmentActivity? = null, completed: (ciphertext: ByteArray?, exception: Exception?) -> Unit)
+ ParameterListWrapping:Hbks.kt$Hbks$(ciphertext: ByteArray?, key: SecretKey?, activity: FragmentActivity? = null, completed: (plaintext: String?, exception: Exception?) -> Unit)
+ ParameterListWrapping:Hbks.kt$Hbks$(plaintext: String?, key: SecretKey?, activity: FragmentActivity? = null, completed: (ciphertext: ByteArray?, exception: Exception?) -> Unit)
+ ParameterListWrapping:InternetArchiveLoginScreen.kt$( state: InternetArchiveLoginState, dispatch: Dispatch<Action> )
+ ParameterListWrapping:Onboarding23SlideFragment.kt$Onboarding23SlideFragment$( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? )
+ ParameterListWrapping:RequestBodyUtil.kt$( cancellable: () -> Boolean, onProgress: (Long) -> Unit = { }, onComplete: () -> Unit = {})
+ ParameterListWrapping:RequestBodyUtil.kt$(cancellable: () -> Boolean, onProgress: (Long) -> Unit = { }, onComplete: () -> Unit = {})
+ ParameterListWrapping:RequestBodyUtil.kt$RequestBodyUtil$( mediaType: MediaType?, inputStream: InputStream, contentLength: Long? = null, listener: RequestListener? )
+ ParameterListWrapping:SnowbirdFileRepository.kt$ISnowbirdFileRepository$( groupKey: String, repoKey: String, forceRefresh: Boolean = false)
+ ParameterListWrapping:SnowbirdFileRepository.kt$ISnowbirdFileRepository$(groupKey: String, repoKey: String, forceRefresh: Boolean = false)
+ ParameterListWrapping:SnowbirdFileRepository.kt$SnowbirdFileRepository$( groupKey: String, repoKey: String, forceRefresh: Boolean)
+ ParameterListWrapping:SnowbirdFileRepository.kt$SnowbirdFileRepository$(groupKey: String, repoKey: String, forceRefresh: Boolean)
+ ParameterListWrapping:SpaceSetupSuccessFragment.kt$SpaceSetupSuccessFragment$( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? )
+ ParameterListWrapping:WebDavFragment.kt$WebDavFragment$( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? )
+ ParameterNaming:BaseDialog.kt$onCheckBoxStateChanged
+ ParameterNaming:HomeScreen.kt$onFolderSelected
+ ParameterNaming:MainDrawerContent.kt$onSelected
+ ParameterNaming:SpaceListScreen.kt$onSpaceClicked
+ ParameterNaming:SpaceSetupScreen.kt$onDwebClicked
+ PrintStackTrace:SnowbirdFileRepository.kt$SnowbirdFileRepository$e
+ PrintStackTrace:SnowbirdGroupRepository.kt$SnowbirdGroupRepository$e
+ PrintStackTrace:SnowbirdService.kt$SnowbirdService$e
+ PrintStackTrace:UnixSocketClient.kt$UnixSocketClient$e
+ PrintStackTrace:VideoRequestHandler.kt$VideoRequestHandler$throwable
+ RethrowCaughtException:UnixSocketClientUtilityExtensions.kt$throw e
+ ReturnCount:BrowseFoldersFragment.kt$BrowseFoldersFragment$private fun addFolder(folder: Folder?)
+ ReturnCount:Conduit.kt$Conduit$fun getProof(): Array<out File>
+ ReturnCount:CreateNewFolderFragment.kt$CreateNewFolderFragment$private fun store()
+ ReturnCount:EmptyableRecyclerView.kt$EmptyableRecyclerView$private fun findSuitableParent(): ViewGroup?
+ ReturnCount:FileUtils.kt$FileUtils$@SuppressLint("NewAPI", "LogNotTimber") fun getPath(context: Context, uri: Uri): String?
+ ReturnCount:FolderAdapter.kt$FolderAdapter.Companion$fun getColorOld(context: Context, highlight: Boolean): Int
+ ReturnCount:GDriveConduit.kt$GDriveConduit$override suspend fun upload(): Boolean
+ ReturnCount:Hbks.kt$Hbks$@RequiresApi(Build.VERSION_CODES.M) fun decrypt( ciphertext: ByteArray?, key: SecretKey?, activity: FragmentActivity? = null, completed: (plaintext: String?, exception: Exception?) -> Unit )
+ ReturnCount:Hbks.kt$Hbks$@RequiresApi(Build.VERSION_CODES.M) fun encrypt( plaintext: String?, key: SecretKey?, activity: FragmentActivity? = null, completed: (ciphertext: ByteArray?, exception: Exception?) -> Unit )
+ ReturnCount:Hbks.kt$Hbks$fun biometryType(context: Context): BiometryType
+ ReturnCount:Hbks.kt$Hbks$fun deviceAvailablity(context: Context): Availability
+ ReturnCount:MainActivity.kt$MainActivity$private fun importSharedMedia(imageIntent: Intent?)
+ ReturnCount:Onboarding23FragmentStateAdapter.kt$Onboarding23FragmentStateAdapter$override fun createFragment(position: Int): Fragment
+ ReturnCount:PasscodeRepository.kt$PasscodeRepository$fun isLockedOut(): Boolean
+ ReturnCount:Picker.kt$Picker$fun import(context: Context, project: Project?, uri: Uri): Media?
+ ReturnCount:UploadManagerActivity.kt$UploadManagerActivity$override fun onOptionsItemSelected(item: MenuItem): Boolean
+ ReturnCount:UploadService.kt$UploadService$private fun isNetworkAvailable(requireUnmetered: Boolean): Boolean
+ ReturnCount:Utility.kt$Utility$fun writeStreamToFile(input: InputStream?, file: File?): Boolean
+ ReturnCount:WebDavConduit.kt$WebDavConduit$@Throws(IOException::class) private suspend fun uploadChunked(base: HttpUrl, path: List<String>, fileName: String): Boolean
+ ReturnCount:WebDavConduit.kt$WebDavConduit$override suspend fun upload(): Boolean
+ SpacingAroundColon:ApiError.kt$ApiError$:
+ SpacingAroundColon:ConsentActivity.kt$ConsentActivity$:
+ SpacingAroundColon:ContentPickerFragment.kt$ContentPickerFragment$:
+ SpacingAroundColon:GeneralSettingsActivity.kt$GeneralSettingsActivity$:
+ SpacingAroundColon:GeneralSettingsActivity.kt$GeneralSettingsActivity.Fragment$:
+ SpacingAroundColon:Hbks.kt$Hbks.Availability.Enroll$:
+ SpacingAroundColon:HomeActivity.kt$HomeActivity$:
+ SpacingAroundColon:HomeScreen.kt$HomeScreenAction.AddMediaClicked$:
+ SpacingAroundColon:JoinGroupResponse.kt$JoinGroupResponse$:
+ SpacingAroundColon:PasscodeEntryViewModel.kt$PasscodeEntryScreenAction.OnSubmit$:
+ SpacingAroundColon:PasscodeManager.kt$PasscodeManager$:
+ SpacingAroundColon:PasscodeSetupViewModel.kt$PasscodeSetupUiAction.OnSubmit$:
+ SpacingAroundColon:PreviewAdapter.kt$PreviewAdapter$:
+ SpacingAroundColon:ProofModeSettingsActivity.kt$ProofModeSettingsActivity.Fragment$:
+ SpacingAroundColon:RequestNameDTO.kt$MembershipRequest$:
+ SpacingAroundColon:RequestNameDTO.kt$RequestName$:
+ SpacingAroundColon:SaveClient.kt$SaveClient.OrbotException$:
+ SpacingAroundColon:SettingsFragment.kt$SettingsFragment$:
+ SpacingAroundColon:SnowbirdCreateGroupFragment.kt$SnowbirdCreateGroupFragment$:
+ SpacingAroundColon:SnowbirdError.kt$SnowbirdError$:
+ SpacingAroundColon:SnowbirdFileItem.kt$SnowbirdFileItem$:
+ SpacingAroundColon:SnowbirdGroupOverviewFragment.kt$SnowbirdGroupOverviewFragment$:
+ SpacingAroundColon:SnowbirdJoinGroupFragment.kt$SnowbirdJoinGroupFragment$:
+ SpacingAroundColon:SnowbirdRepoListAdapter.kt$SnowbirdRepoListAdapter$:
+ SpacingAroundColon:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment$:
+ SpacingAroundColon:SnowbirdShareFragment.kt$SnowbirdShareFragment$:
+ SpacingAroundColon:SwipeToDeleteCallback.kt$SwipeToDeleteCallback$:
+ SpacingAroundColon:UnixSocketAPI.kt$UnixSocketAPI$:
+ SpacingAroundColon:WebDavSetupLicenseFragment.kt$WebDavSetupLicenseFragment$:
+ SpacingAroundKeyword:BrowseFoldersViewModel.kt$BrowseFoldersViewModel$else
+ SpacingAroundKeyword:Context.kt$catch
+ SpacingAroundKeyword:Drawable.kt$else
+ SpacingAroundKeyword:DrawableExtensions.kt$else
+ SpacingAroundKeyword:GDriveFragment.kt$GDriveFragment$if
+ SpacingAroundKeyword:Hbks.kt$Hbks$catch
+ SpacingAroundKeyword:Hbks.kt$Hbks$else
+ SpacingAroundKeyword:InternetArchiveDetailsViewModel.kt$InternetArchiveDetailsViewModel$when
+ SpacingAroundKeyword:PackageManager.kt$else
+ SpacingAroundKeyword:Picker.kt$Picker$else
+ SpacingAroundKeyword:ProofModeHelper.kt$ProofModeHelper$catch
+ SpacingAroundKeyword:ProofModeHelper.kt$ProofModeHelper$else
+ SpacingAroundKeyword:ReviewActivity.kt$ReviewActivity$else
+ SpacingAroundKeyword:ReviewActivity.kt$ReviewActivity.<no name provided>$else
+ SpacingAroundKeyword:SaveClient.kt$SaveClient.Companion$else
+ SpacingAroundKeyword:SaveClient.kt$SaveClient.Companion.<no name provided>$else
+ SpacingAroundKeyword:SpaceAdapter.kt$SpaceAdapter$else
+ SpacingAroundKeyword:SpaceDrawerAdapter.kt$SpaceDrawerAdapter.SpaceViewHolder$if
+ SpacingAroundKeyword:UploadManagerActivity.kt$UploadManagerActivity$else
+ SpacingAroundKeyword:UploadManagerActivity.kt$UploadManagerActivity.<no name provided>$else
+ SpacingAroundKeyword:Util.kt$Util$else
+ SpacingAroundKeyword:Utility.kt$Utility$catch
+ SpacingAroundKeyword:Utility.kt$Utility$finally
+ SpacingAroundKeyword:View.kt$ViewHelper$else
+ SpacingAroundKeyword:View.kt$else
+ SpacingAroundKeyword:ViewExtension.kt$ViewHelper$else
+ SpacingAroundKeyword:ViewExtension.kt$else
+ SpacingAroundKeyword:WebDavConduit.kt$WebDavConduit$catch
+ SpacingAroundKeyword:WebDavFragment.kt$WebDavFragment.<no name provided>$if
+ SpacingAroundKeyword:WebDavSetupLicenseFragment.kt$WebDavSetupLicenseFragment$if
+ SpacingAroundOperators:MediaCacheScreen.kt$=
+ SpacingAroundOperators:PasscodeSetupViewModel.kt$PasscodeSetupViewModel$->
+ SpacingAroundParens:FileUploadResult.kt$FileUploadResult$(
+ SpacingAroundParens:Picker.kt$Picker$(
+ SpacingAroundParens:ReviewActivity.kt$ReviewActivity$(
+ SpacingAroundParens:SnowbirdConduit.kt$SnowbirdConduit$(
+ SpacingAroundParens:SnowbirdFileListAdapter.kt$SnowbirdFileListAdapter$(
+ SpacingAroundParens:WebDAVModel.kt$BackendCapabilities$(
+ SpacingAroundParens:WebDAVModel.kt$Data$(
+ SpacingAroundParens:WebDAVModel.kt$Meta$(
+ SpacingAroundParens:WebDAVModel.kt$Ocs$(
+ SpacingAroundParens:WebDAVModel.kt$Quota$(
+ SpacingAroundParens:WebDAVModel.kt$WebDAVModel$(
+ SpacingBetweenDeclarationsWithAnnotations:BasicAuthInterceptor.kt$BasicAuthInterceptor$@Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response
+ SpacingBetweenDeclarationsWithAnnotations:Hbks.kt$Hbks.Availability$Enroll : Availability
+ SpacingBetweenDeclarationsWithAnnotations:Media.kt$Media.Status$DeleteRemote : Status
+ SpacingBetweenDeclarationsWithAnnotations:Media.kt$Media.Status$Published : Status
+ SpacingBetweenDeclarationsWithAnnotations:VideoRequestHandler.kt$VideoRequestHandler.Companion$@Throws(Throwable::class) fun retrieveVideoFrameFromVideo(context: Context?, videoPath: Uri?): Bitmap?
+ SpacingBetweenDeclarationsWithComments:Prefs.kt$Prefs$// private const val USE_NEXTCLOUD_CHUNKING = "upload_nextcloud_chunks"
+ SpacingBetweenDeclarationsWithComments:UnixSocketClient.kt$UnixSocketClient$// val socketPath: String = File(context.filesDir, "rust_server.sock").absolutePath
+ SpreadOperator:GDriveConduit.kt$GDriveConduit.Companion$( GoogleSignIn.getLastSignedInAccount(context), *SCOPES )
+ SpreadOperator:GDriveFragment.kt$GDriveFragment$( requireActivity(), REQUEST_CODE_GOOGLE_AUTH, GoogleSignIn.getLastSignedInAccount(requireActivity()), *GDriveConduit.SCOPES )
+ StringTemplate:Hbks.kt$Hbks$${algorithm}
+ StringTemplate:Hbks.kt$Hbks$${blockMode}
+ StringTemplate:Hbks.kt$Hbks$${padding}
+ StringTemplate:MainMediaViewHolder.kt$MainMediaViewHolder$${progressValue}
+ StringTemplate:MediaViewHolder.kt$MediaViewHolder$${progressValue}
+ StringTemplate:PreviewViewHolder.kt$PreviewViewHolder$${progressValue}
+ StringTemplate:Utility.kt$Utility$${appId}
+ SwallowedException:Context.kt$e: ActivityNotFoundException
+ SwallowedException:PackageManager.kt$e: PackageManager.NameNotFoundException
+ SwallowedException:ProofModeSettingsActivity.kt$ProofModeSettingsActivity.Companion$ioe: IOException
+ SwallowedException:RestEndpointTask.kt$RestEndpointTask$e: Exception
+ SwallowedException:SnowbirdFileItem.kt$SnowbirdFileItem.Companion$e: SQLiteException
+ SwallowedException:SnowbirdFileViewModel.kt$SnowbirdFileViewModel$e: TimeoutCancellationException
+ SwallowedException:SnowbirdGroup.kt$SnowbirdGroup.Companion$e: SQLiteException
+ SwallowedException:SnowbirdGroupViewModel.kt$SnowbirdGroupViewModel$e: TimeoutCancellationException
+ SwallowedException:SnowbirdRepoViewModel.kt$SnowbirdRepoViewModel$e: TimeoutCancellationException
+ SwallowedException:UnixSocketClient.kt$UnixSocketClient$e: Exception
+ SwallowedException:UnixSocketClientFileExtensions.kt$e: Exception
+ SwallowedException:VideoRequestHandler.kt$VideoRequestHandler.Companion$e: Exception
+ ThrowingExceptionsWithoutMessageOrCause:Hbks.kt$Hbks$NullPointerException()
+ ThrowingExceptionsWithoutMessageOrCause:Onboarding23FragmentStateAdapter.kt$Onboarding23FragmentStateAdapter$IndexOutOfBoundsException()
+ ThrowsCount:UnixSocketClient.kt$UnixSocketClient$fun <REQUEST : SerializableMarker, RESPONSE : Any> sendRequestInternal( endpoint: String, method: HttpMethod, body: REQUEST?, serialize: (REQUEST) -> String, deserialize: (String) -> RESPONSE ): RESPONSE
+ TooGenericExceptionCaught:BrowseFoldersViewModel.kt$BrowseFoldersViewModel$e: Throwable
+ TooGenericExceptionCaught:GDriveConduit.kt$GDriveConduit$e: Exception
+ TooGenericExceptionCaught:Hbks.kt$Hbks$e: Exception
+ TooGenericExceptionCaught:IaConduit.kt$IaConduit$e: Throwable
+ TooGenericExceptionCaught:MainMediaViewHolder.kt$MainMediaViewHolder$e: Throwable
+ TooGenericExceptionCaught:MediaViewHolder.kt$MediaViewHolder$e: Throwable
+ TooGenericExceptionCaught:Picker.kt$Picker$e: Exception
+ TooGenericExceptionCaught:PreviewViewHolder.kt$PreviewViewHolder$e: Throwable
+ TooGenericExceptionCaught:ProofModeHelper.kt$ProofModeHelper$e: Exception
+ TooGenericExceptionCaught:RestEndpointTask.kt$RestEndpointTask$e: Exception
+ TooGenericExceptionCaught:SnowbirdFileRepository.kt$SnowbirdFileRepository$e: Exception
+ TooGenericExceptionCaught:SnowbirdGroupRepository.kt$SnowbirdGroupRepository$e: Exception
+ TooGenericExceptionCaught:SnowbirdRepoRepository.kt$SnowbirdRepoRepository$e: Exception
+ TooGenericExceptionCaught:SnowbirdService.kt$SnowbirdService$e: Exception
+ TooGenericExceptionCaught:StringExtensions.kt$e: Exception
+ TooGenericExceptionCaught:SuspendableExtensions.kt$e: Throwable
+ TooGenericExceptionCaught:UnixSocketClient.kt$UnixSocketClient$e: Exception
+ TooGenericExceptionCaught:UnixSocketClientFileExtensions.kt$e: Exception
+ TooGenericExceptionCaught:VideoRequestHandler.kt$VideoRequestHandler$throwable: Throwable
+ TooGenericExceptionCaught:VideoRequestHandler.kt$VideoRequestHandler.Companion$e: Exception
+ TooGenericExceptionCaught:WebDavConduit.kt$WebDavConduit$e: Throwable
+ TooGenericExceptionThrown:Conduit.kt$Conduit$throw Exception("Cancelled")
+ TooGenericExceptionThrown:GDriveConduit.kt$GDriveConduit$throw Exception("Cancelled")
+ TooGenericExceptionThrown:GDriveConduit.kt$GDriveConduit.Companion$throw Exception("could not create folders $destinationPath")
+ TooGenericExceptionThrown:IaConduit.kt$IaConduit$throw RuntimeException("${result.code}: ${result.message}")
+ TooGenericExceptionThrown:VideoRequestHandler.kt$VideoRequestHandler.Companion$throw Throwable("Exception in retrieveVideoFrameFromVideo(String videoPath)" + e.message)
+ TooGenericExceptionThrown:WebDavConduit.kt$WebDavConduit$throw Exception("Cancelled")
+ TooManyFunctions:AppLogger.kt$AppLogger
+ TooManyFunctions:Conduit.kt$Conduit
+ TooManyFunctions:FoldersActivity.kt$FoldersActivity : BaseActivityFolderAdapterListener
+ TooManyFunctions:HomeActivity.kt$HomeActivity : FragmentActivity
+ TooManyFunctions:MainActivity.kt$MainActivity : BaseActivitySpaceDrawerAdapterListenerFolderDrawerAdapterListener
+ TooManyFunctions:MainMediaAdapter.kt$MainMediaAdapter : Adapter
+ TooManyFunctions:MainMediaFragment.kt$MainMediaFragment : Fragment
+ TooManyFunctions:MainMediaScreen.kt$net.opendasharchive.openarchive.features.main.ui.MainMediaScreen.kt
+ TooManyFunctions:MediaAdapter.kt$MediaAdapter : Adapter
+ TooManyFunctions:PasscodeRepository.kt$PasscodeRepository
+ TooManyFunctions:PreviewActivity.kt$PreviewActivity : BaseActivityOnClickListenerListener
+ TooManyFunctions:SnowbirdCreateGroupFragment.kt$SnowbirdCreateGroupFragment : BaseFragment
+ TooManyFunctions:SnowbirdFileListFragment.kt$SnowbirdFileListFragment : BaseFragment
+ TooManyFunctions:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment : BaseFragment
+ TooManyFunctions:SnowbirdJoinGroupFragment.kt$SnowbirdJoinGroupFragment : BaseFragment
+ TooManyFunctions:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment : BaseFragment
+ TooManyFunctions:SnowbirdService.kt$SnowbirdService : Service
+ TooManyFunctions:WebDavFragment.kt$WebDavFragment : BaseFragment
+ UnusedParameter:AppLogger.kt$AppLogger$context: Context
+ UnusedParameter:AppLogger.kt$AppLogger$initDebugger: Boolean
+ UnusedParameter:BrowseFolderScreen.kt$onClick: () -> Unit
+ UnusedParameter:BrowseFoldersViewModel.kt$BrowseFoldersViewModel$space: Space
+ UnusedParameter:HomeActivity.kt$HomeActivity$folderId: Long
+ UnusedParameter:HomeScreen.kt$onAddMedia: (AddMediaType) -> Unit
+ UnusedParameter:HomeScreen.kt$onFolderSelected: (Long) -> Unit
+ UnusedParameter:HomeScreen.kt$onNewFolder: () -> Unit
+ UnusedParameter:InternetArchiveHeader.kt$titleSize: TextUnit = 18.sp
+ UnusedParameter:InternetArchiveLoginScreen.kt$enabled: Boolean = true
+ UnusedParameter:MainActivity.kt$MainActivity$count: Int
+ UnusedParameter:MainDrawerContent.kt$isSelected: Boolean = false
+ UnusedParameter:MainDrawerContent.kt$onSelected: () -> Unit
+ UnusedParameter:MainDrawerContent.kt$project: Project
+ UnusedParameter:MainMediaAdapterTest.kt$progress: Int? = 0
+ UnusedParameter:PasscodeEntryScreen.kt$onExit: () -> Unit
+ UnusedParameter:SnowbirdBridge.kt$SnowbirdBridge.Companion$message: String
+ UnusedParameter:Space.kt$Space$style: IconStyle = IconStyle.SOLID
+ UnusedParameter:Utility.kt$Utility$appId: String
+ UnusedPrivateMember:AddFolderScreen.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun AddFolderScreenPreview()
+ UnusedPrivateMember:BaseButton.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun CustomButtonPreview()
+ UnusedPrivateMember:BaseButton.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun CustomDestructiveButtonPreview()
+ UnusedPrivateMember:BaseButton.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun CustomNeutralButtonPreview()
+ UnusedPrivateMember:BaseDialog.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun BaseDialogPreview()
+ UnusedPrivateMember:BaseDialog.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun ErrorDialogPreview()
+ UnusedPrivateMember:BaseDialog.kt$@Preview @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun WarningDialogPreview()
+ UnusedPrivateMember:BrowseFolderScreen.kt$@Preview @Composable private fun BrowseFolderScreenPreview()
+ UnusedPrivateMember:ExpandableSpaceList.kt$@Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun ExpandableSpaceListPreview()
+ UnusedPrivateMember:FolderOptionsPopup.kt$@Preview @Composable private fun FolderOptionsPopupPreview()
+ UnusedPrivateMember:HomeScreen.kt$@Preview @Composable private fun MainContentPreview()
+ UnusedPrivateMember:IaConduit.kt$IaConduit$@Throws(IOException::class) private fun OkHttpClient.uploadProofFiles(uploadFile: File)
+ UnusedPrivateMember:InternetArchiveDetailsScreen.kt$@Composable @Preview(showBackground = true) @Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) private fun InternetArchiveScreenPreview()
+ UnusedPrivateMember:InternetArchiveHeader.kt$@Composable @Preview(showBackground = true) @Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) private fun InternetArchiveHeaderPreview()
+ UnusedPrivateMember:InternetArchiveLoginScreen.kt$@Composable @Preview @Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) private fun InternetArchiveLoginPreview()
+ UnusedPrivateMember:MainDrawerContent.kt$@Preview @Composable private fun MainDrawerContentPreview()
+ UnusedPrivateMember:MainMediaAdapter.kt$MainMediaAdapter$private fun selectView(view: View)
+ UnusedPrivateMember:MainMediaScreen.kt$private fun deleteMediaItem(sections: MutableList<CollectionSection>, media: Media)
+ UnusedPrivateMember:MainMediaScreen.kt$private fun deleteSelected(sections: MutableList<CollectionSection>, context: Context)
+ UnusedPrivateMember:NumericKeypad.kt$@Preview @Composable private fun NumericKeypadPreview()
+ UnusedPrivateMember:PasscodeDots.kt$@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview @Composable private fun PasswordDotsPreview()
+ UnusedPrivateMember:PasscodeEntryScreen.kt$@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview @Composable private fun PasscodeEntryScreenPreview()
+ UnusedPrivateMember:PasscodeSetupScreen.kt$@Preview(uiMode = UI_MODE_NIGHT_YES) @Preview @Composable private fun PasscodeSetupScreenPreview()
+ UnusedPrivateMember:PrimaryButton.kt$@Preview @Composable private fun PrimaryButtonPreview()
+ UnusedPrivateMember:ProofModeScreen.kt$@Preview @Composable private fun ProofModeScreenPreview()
+ UnusedPrivateMember:ProofModeSettingsActivity.kt$ProofModeSettingsActivity.Companion$private fun shareKey(activity: Activity)
+ UnusedPrivateMember:ServerOptionItem.kt$@Preview @Composable private fun ServerOptionItemPreview()
+ UnusedPrivateMember:SettingsScreen.kt$@Preview @Composable private fun SettingsScreenPreview()
+ UnusedPrivateMember:SpaceListScreen.kt$@Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun SpaceListScreenPreview()
+ UnusedPrivateMember:SpaceSetupScreen.kt$@Preview @Composable private fun SpaceSetupScreenPreview()
+ UnusedPrivateProperty:BrowseFolderScreen.kt$val navController = LocalView.current.findNavController()
+ UnusedPrivateProperty:Colors.kt$private val c23_grey_50 = Color(0xff777979)
+ UnusedPrivateProperty:Colors.kt$private val c23_nav_drawer_night = Color(0xff101010)
+ UnusedPrivateProperty:Colors.kt$private val c23_teal_10 = Color(0xff001b19) // v=10.6 -->
+ UnusedPrivateProperty:Colors.kt$private val c23_teal_100 = Color(0xff00ffeb) // h=175,3 s=100 v=100 -->
+ UnusedPrivateProperty:Colors.kt$private val c23_teal_30 = Color(0xff004e48) // v=30.6 -->
+ UnusedPrivateProperty:Colors.kt$private val c23_teal_50 = Color(0xff008177) // v=50.6 -->
+ UnusedPrivateProperty:Colors.kt$private val c23_teal_60 = Color(0xff009b8f) // v=60.6 -->
+ UnusedPrivateProperty:Colors.kt$private val c23_teal_80 = Color(0xff00cebe) // v=80.6 -->
+ UnusedPrivateProperty:Colors.kt$private val c23_teal_90 = Color(0xff00e7d5) // v=90.6 -->
+ UnusedPrivateProperty:Colors.kt$private val darkPrimary = Color(0xff000A0A)
+ UnusedPrivateProperty:GDriveConduit.kt$GDriveConduit$val response = request.execute()
+ UnusedPrivateProperty:HomeActivity.kt$HomeActivity$private val folderResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { val selectedFolderId: Long? = result.data?.getLongExtra("SELECTED_FOLDER_ID", -1) if (selectedFolderId != null && selectedFolderId > -1) { navigateToFolder(selectedFolderId) } } }
+ UnusedPrivateProperty:HomeActivity.kt$HomeActivity$private val mNewFolderResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == RESULT_OK) { // TODO: Refresh projects in MainViewModel } }
+ UnusedPrivateProperty:IaConduit.kt$IaConduit.Companion$private const val ARCHIVE_DETAILS_ENDPOINT = "https://archive.org/details/"
+ UnusedPrivateProperty:MainActivity.kt$MainActivity$private var currentSelectionCount = 0
+ UnusedPrivateProperty:MainMediaAdapter.kt$MainMediaAdapter.Companion$private const val PAYLOAD_PROGRESS = "progress"
+ UnusedPrivateProperty:MainMediaAdapter.kt$MainMediaAdapter.Companion$private const val PAYLOAD_SELECTION = "selection"
+ UnusedPrivateProperty:MainMediaScreen.kt$var isSelecting by remember { mutableStateOf(false) }
+ UnusedPrivateProperty:MainMediaScreen.kt$var showDeleteDialog by remember { mutableStateOf(false) }
+ UnusedPrivateProperty:NumericKeypad.kt$val borderColor by animateColorAsState( targetValue = when { isPressed -> when (label) { "delete" -> colorResource(R.color.red_bg).copy(alpha = 0.7f) "submit" -> MaterialTheme.colorScheme.tertiary.copy(alpha = 0.7f) else -> MaterialTheme.colorScheme.primary.copy(alpha = 0.5f) } else -> when (label) { "delete" -> colorResource(R.color.red_bg).copy(alpha = 0.5f) "submit" -> MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f) else -> Color.Transparent } }, animationSpec = spring(), label = "" )
+ UnusedPrivateProperty:ProofModeScreen.kt$val permissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission() ) { isGranted -> if (!isGranted) { Toast.makeText(context, "Please allow all permissions", Toast.LENGTH_LONG).show() val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val uri = Uri.fromParts("package", context.packageName, null) intent.data = uri context.startActivity(intent) } }
+ UnusedPrivateProperty:ProofModeScreen.kt$val uriHandler = LocalUriHandler.current
+ UnusedPrivateProperty:SectionViewHolder.kt$SectionViewHolder.Companion$private val mDf = DateFormat.getDateTimeInstance()
+ UnusedPrivateProperty:SnowbirdFragment.kt$SnowbirdFragment$private val CANNED_URI = "save+dweb::?dht=82fd345d484393a96b6e0c5d5e17a85a61c9184cc5a3311ab069d6efa0bf1410&enc=6fa27396fe298f92c91013ac54d8f316c2d45dc3bed0edec73078040aa10feed&pk=f4b404d294817cf11ea7f8ef7231626e03b74f6fafe3271b53918608afa82d12&sk=5482a8f490081be684fbadb8bde7f0a99bab8acdcf1ec094826f0f18e327e399"
+ UnusedPrivateProperty:SnowbirdFragment.kt$SnowbirdFragment$private var canNavigate = false
+ UnusedPrivateProperty:SnowbirdGroupRepository.kt$SnowbirdGroupRepository$val shouldFetchFromNetwork = forceRefresh || currentTime - lastFetchTime > cacheValidityPeriod
+ UnusedPrivateProperty:UnixSocketClient.kt$UnixSocketClient$context: Context
+ VariableNaming:SnowbirdFragment.kt$SnowbirdFragment$private val CANNED_URI = "save+dweb::?dht=82fd345d484393a96b6e0c5d5e17a85a61c9184cc5a3311ab069d6efa0bf1410&enc=6fa27396fe298f92c91013ac54d8f316c2d45dc3bed0edec73078040aa10feed&pk=f4b404d294817cf11ea7f8ef7231626e03b74f6fafe3271b53918608afa82d12&sk=5482a8f490081be684fbadb8bde7f0a99bab8acdcf1ec094826f0f18e327e399"
+ ViewModelForwarding:HomeScreen.kt$HomeScreen( viewModel = viewModel, onExit = onExit, onNewFolder = onNewFolder, onFolderSelected = onFolderSelected, onAddMedia = onAddMedia, onNavigateToCache = { navController.navigate(MediaCacheRoute) } )
+ WildcardImport:BadgeDrawable.kt$import android.graphics.*
+ WildcardImport:CleanInsightsManager.kt$import org.cleaninsights.sdk.*
+ WildcardImport:Hbks.kt$import java.security.*
+ WildcardImport:Hbks.kt$import javax.crypto.*
+ WildcardImport:IaConduit.kt$import okhttp3.*
+ WildcardImport:MediaCacheScreen.kt$import androidx.compose.foundation.layout.*
+ WildcardImport:RequestBodyUtil.kt$import java.io.*
+ WildcardImport:UploadService.kt$import android.app.*
+ Wrapping:BaseDialog.kt$(
+ Wrapping:BrowseFoldersFragment.kt$BrowseFoldersFragment$(
+ Wrapping:BrowseFoldersFragment.kt$BrowseFoldersFragment$(RESULT_OK, Intent().apply { putExtra(AddFolderActivity.EXTRA_FOLDER_ID, project.id) })
+ Wrapping:CleanInsightsManager.kt$CleanInsightsManager$(
+ Wrapping:CleanInsightsManager.kt$CleanInsightsManager$(CI_CAMPAIGN, object : ConsentRequestUi { override fun show( campaignId: String, campaign: Campaign, complete: ConsentRequestUiComplete ) { mCompleted = completed context.startActivity(Intent(context, ConsentActivity::class.java)) } override fun show(feature: Feature, complete: ConsentRequestUiComplete) { complete(true) } }, completed)
+ Wrapping:ConsentActivity.kt$ConsentActivity$(
+ Wrapping:Drawable.kt$(
+ Wrapping:EditFolderActivity.kt$EditFolderActivity$(
+ Wrapping:EditFolderActivity.kt$EditFolderActivity$(this, R.string.action_remove_project, R.string.remove_from_app, buttons = listOf( AlertHelper.positiveButton(R.string.remove) { _, _ -> mProject.delete() finish() }, AlertHelper.negativeButton()))
+ Wrapping:FileUtils.kt$FileUtils$(
+ Wrapping:GDriveActivity.kt$GDriveActivity$(
+ Wrapping:GDriveActivity.kt$GDriveActivity$(this, R.string.are_you_sure_you_want_to_remove_this_server_from_the_app, R.string.remove_from_app, buttons = listOf( AlertHelper.positiveButton(R.string.remove) { _, _ -> // delete sign-in from database space.delete() // google logout val googleSignInClient = GoogleSignIn.getClient(applicationContext, GoogleSignInOptions.DEFAULT_SIGN_IN) googleSignInClient.revokeAccess().addOnCompleteListener { googleSignInClient.signOut() } // leave activity Space.navigate(this) }, AlertHelper.negativeButton() ))
+ Wrapping:GDriveFragment.kt$GDriveFragment$( getString( R.string.gdrive_disclaimer_1, getString(R.string.app_name), getString(R.string.google_name), getString(R.string.gdrive_sudp_name), ), HtmlCompat.FROM_HTML_MODE_COMPACT )
+ Wrapping:Hbks.kt$Hbks$(
+ Wrapping:Hbks.kt$Hbks$(activity, object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError( errorCode: Int, errString: CharSequence ) { super.onAuthenticationError(errorCode, errString) completed(false) } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) completed(true) } override fun onAuthenticationFailed() { super.onAuthenticationFailed() completed(false) } })
+ Wrapping:InternetArchiveActivity.kt$InternetArchiveActivity$(
+ Wrapping:MainMediaScreen.kt${ /* no op */ }
+ Wrapping:Media.kt$Media.Companion$(
+ Wrapping:MediaAdapter.kt$MediaAdapter$( it, it.getString(R.string.upload_unsuccessful_description), R.string.upload_unsuccessful, R.drawable.ic_error, listOf( AlertHelper.positiveButton(R.string.retry) { _, _ -> media[pos].apply { sStatus = Media.Status.Queued statusMessage = "" save() BroadcastManager.postChange(it, collectionId, id) } UploadService.startUploadService(it) }, AlertHelper.negativeButton(R.string.remove) { _, _ -> deleteItem(pos) }, AlertHelper.neutralButton() ) )
+ Wrapping:MediaCacheScreen.kt$(
+ Wrapping:Onboarding23InstructionsActivity.kt$Onboarding23InstructionsActivity$(
+ Wrapping:Onboarding23InstructionsActivity.kt$Onboarding23InstructionsActivity$(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { if (isFirstPage()) { finish() } else { mBinding.viewPager.currentItem-- } } })
+ Wrapping:PasscodeSetupActivity.kt$PasscodeSetupActivity$(
+ Wrapping:PasscodeSetupActivity.kt$PasscodeSetupActivity$(RESULT_OK, Intent().apply { putExtra(EXTRA_PASSCODE_ENABLED, true) })
+ Wrapping:Picker.kt$Picker$(
+ Wrapping:Picker.kt$Picker$(activity, arrayOf( Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO ))
+ Wrapping:ProofModeHelper.kt$ProofModeHelper$(
+ Wrapping:RequestBodyUtil.kt$
+ Wrapping:RequestBodyUtil.kt$RequestBodyUtil$(
+ Wrapping:SnowbirdFileItem.kt$SnowbirdFileItem.Companion$(
+ Wrapping:SnowbirdFileListFragment.kt$SnowbirdFileListFragment$(
+ Wrapping:SnowbirdFileListFragment.kt$SnowbirdFileListFragment$(object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.menu_snowbird, menu) } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { return when (menuItem.itemId) { R.id.action_add -> { Timber.d("Adde!") openFilePicker() true } else -> false } } }, viewLifecycleOwner, Lifecycle.State.RESUMED)
+ Wrapping:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment$(
+ Wrapping:SnowbirdGroupListFragment.kt$SnowbirdGroupListFragment$(object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.menu_snowbird, menu) } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { return when (menuItem.itemId) { R.id.action_add -> { if (isJetpackNavigation) { val action = SnowbirdGroupListFragmentDirections.actionFragmentSnowbirdGroupListToFragmentSnowbirdCreateGroup() findNavController().navigate(action) } else { setFragmentResult( RESULT_REQUEST_KEY, bundleOf(RESULT_BUNDLE_NAVIGATION_KEY to RESULT_VAL_RAVEN_CREATE_GROUP_SCREEN) ) } true } else -> false } } }, viewLifecycleOwner, Lifecycle.State.RESUMED)
+ Wrapping:SnowbirdGroupRepository.kt$SnowbirdGroupRepository$(
+ Wrapping:SnowbirdRepo.kt$SnowbirdRepo.Companion$(
+ Wrapping:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment$(
+ Wrapping:SnowbirdRepoListFragment.kt$SnowbirdRepoListFragment$(object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.menu_snowbird, menu) } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { return when (menuItem.itemId) { R.id.action_add -> { Utility.showMaterialWarning( context = requireContext(), message = "Feature not implemented yet.", positiveButtonText = "OK" ) true } else -> false } } }, viewLifecycleOwner, Lifecycle.State.RESUMED)
+ Wrapping:SpaceAdapter.kt$SpaceAdapter$(
+ Wrapping:TextView.kt$(
+ Wrapping:TextView.kt$(SpannableString(text).apply { setSpan(URLSpan(""), 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) }, TextView.BufferType.SPANNABLE)
+ Wrapping:TwoLetterDrawable.kt$TwoLetterDrawable$(
+ Wrapping:UnixSocketAPI.kt$UnixSocketAPI$(
+ Wrapping:Utility.kt$Utility$(
+ Wrapping:WebDavFragment.kt$WebDavFragment$(
+
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2459bf39..7e17a5a8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -64,6 +64,7 @@
@@ -100,11 +101,43 @@
+ android:name=".features.main.HomeActivity"
+ android:exported="true"
+ android:theme="@style/SaveAppTheme.NoActionBar"
+ android:screenOrientation="portrait">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+ android:theme="@style/SaveAppTheme.NoActionBar.Onboarding" />
-
+
+
+
+
+
(DIFF_CALLBACK), FolderAdapterListener {
-
- inner class ViewHolder(private val binding: RvFoldersRowBinding) : RecyclerView.ViewHolder(binding.root) {
-
- fun bind(listener: WeakReference?, project: Project?) {
-
- val isSelected = listener?.get()?.getSelectedProject()?.id == project?.id
- itemView.isSelected = isSelected
-
- val textColorRes = if (isSelected) R.color.colorTertiary else R.color.colorText
- val iconColorRes = if (isSelected) R.color.colorTertiary else R.color.colorOnBackground
- val backgroundRes = if (isSelected) R.drawable.item_background_selector else android.R.color.transparent
+class FolderAdapter(private val context: Context, private val listener: FolderAdapterListener, private val isArchived: Boolean = false) : ListAdapter(DIFF_CALLBACK) {
- binding.root.setBackgroundResource(backgroundRes)
+ inner class FolderViewHolder(private val binding: RvFoldersRowBinding) :
+ RecyclerView.ViewHolder(binding.root) {
- binding.rvTitle.text = project?.description
- binding.rvTitle.setTextColor(ContextCompat.getColor(context, textColorRes))
+ fun bind(project: Project) {
- val icon = if (isSelected) {
- ContextCompat.getDrawable(context, R.drawable.baseline_folder_white_24)
- } else {
- ContextCompat.getDrawable(context, R.drawable.outline_folder_white_24)
- }
+ binding.rvTitle.text = project.description
- icon?.setTint(ContextCompat.getColor(context, iconColorRes))
+ val icon = ContextCompat.getDrawable(context, R.drawable.ic_folder_new)
binding.rvIcon.setImageDrawable(icon)
- if (isArchived) {
- binding.rvEdit.visibility = View.GONE
- } else {
- binding.rvEdit.visibility = View.VISIBLE
- }
-
-
-
- if (project != null) {
- binding.textContainer.setOnClickListener {
- if (isArchived) {
- listener?.get()?.projectEdit(project)
- } else {
- listener?.get()?.projectClicked(project)
- }
- }
-
- binding.rvEdit.setOnClickListener {
- listener?.get()?.projectEdit(project)
- }
-
- } else {
- binding.root.setOnClickListener(null)
+ itemView.setOnClickListener {
+ listener.projectClicked(project)
}
}
}
@@ -91,48 +48,10 @@ class FolderAdapter(
return oldItem.description == newItem.description
}
}
-
- private var highlightColor: Int? = null
- private var defaultColor: Int? = null
-
- fun getColorOld(context: Context, highlight: Boolean): Int {
- if (highlight) {
- var color = highlightColor
-
- if (color != null) return color
-
- color = ContextCompat.getColor(context, R.color.colorPrimary)
- highlightColor = color
-
- return color
- }
-
- var color = defaultColor
-
- if (color != null) return color
-
- val textview = TextView(context)
- color = textview.currentTextColor
- defaultColor = color
-
- return color
- }
-
- fun getColor(context: Context, highlight: Boolean): Int {
- return if (highlight) {
- ContextCompat.getColor(context, R.color.colorPrimary)
- } else {
- ContextCompat.getColor(context, R.color.colorOnBackground)
- }
- }
}
- private val mListener: WeakReference? = WeakReference(listener)
-
- private var mLastSelected: Project? = null
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- return ViewHolder(
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
+ return FolderViewHolder(
RvFoldersRowBinding.inflate(
LayoutInflater.from(parent.context),
parent, false
@@ -140,43 +59,14 @@ class FolderAdapter(
)
}
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
val project = getItem(position)
- holder.bind(WeakReference(this), project)
+ holder.bind( project)
}
fun update(projects: List) {
- notifyItemChanged(getIndex(mLastSelected))
submitList(projects)
}
-
- override fun projectClicked(project: Project) {
- notifyItemChanged(getIndex(getSelectedProject()))
- notifyItemChanged(getIndex(project))
-
- mListener?.get()?.projectClicked(project)
- }
-
- override fun getSelectedProject(): Project? {
- mLastSelected = mListener?.get()?.getSelectedProject()
-
- return mLastSelected
- }
-
- override fun projectEdit(project: Project) {
- notifyItemChanged(getIndex(getSelectedProject()))
- notifyItemChanged(getIndex(project))
-
- mListener?.get()?.projectEdit(project)
- }
-
- private fun getIndex(project: Project?): Int {
- return if (project == null) {
- -1
- } else {
- currentList.indexOf(project)
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt
index 764c0327..21bc61c8 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt
@@ -4,13 +4,15 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.util.Log
-import coil.Coil
-import coil.ImageLoader
-import coil.util.Logger
+import coil3.ImageLoader
+import coil3.PlatformContext
+import coil3.SingletonImageLoader
+import coil3.util.Logger
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.core.di.passcodeModule
import net.opendasharchive.openarchive.core.di.retrofitModule
import net.opendasharchive.openarchive.core.di.unixSocketModule
import net.opendasharchive.openarchive.core.logger.AppLogger
@@ -24,7 +26,7 @@ import org.koin.core.context.startKoin
import org.koin.core.logger.Level
import timber.log.Timber
-class SaveApp : SugarApp() {
+class SaveApp : SugarApp(), SingletonImageLoader.Factory {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
@@ -42,31 +44,17 @@ class SaveApp : SugarApp() {
coreModule,
featuresModule,
retrofitModule,
- unixSocketModule
+ unixSocketModule,
+ passcodeModule
)
}
- val imageLoader = ImageLoader.Builder(this)
- .logger(object : Logger {
- override var level = Log.VERBOSE
-
- override fun log(
- tag: String,
- priority: Int,
- message: String?,
- throwable: Throwable?
- ) {
- Timber.tag("Coil").log(priority, throwable, message)
- }
- })
- .build()
-
- Coil.setImageLoader(imageLoader)
Prefs.load(this)
if (Prefs.useTor) initNetCipher()
- Theme.set(Prefs.theme)
+ val useDarkMode = Prefs.getBoolean(getString(R.string.pref_key_use_dark_mode), false)
+ Theme.darkModeEnabled = useDarkMode
CleanInsightsManager.init(this)
@@ -112,4 +100,20 @@ class SaveApp : SugarApp() {
const val TOR_SERVICE_ID = 2602
const val TOR_SERVICE_CHANNEL = "tor_service_channel"
}
+
+ override fun newImageLoader(context: PlatformContext): ImageLoader {
+ return ImageLoader.Builder(this).logger(object : Logger {
+ override var minLevel: Logger.Level = Logger.Level.Verbose
+
+ override fun log(
+ tag: String,
+ level: Logger.Level,
+ message: String?,
+ throwable: Throwable?
+ ) {
+ Timber.tag("Coil3:$tag").log(level.ordinal, throwable, message)
+ }
+ })
+ .build()
+ }
}
\ 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
index d3851cc4..d3156218 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt
@@ -1,8 +1,14 @@
package net.opendasharchive.openarchive.core.di
+import android.content.Context
+import com.google.api.services.drive.Drive
+import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
import net.opendasharchive.openarchive.features.core.dialog.DefaultResourceProvider
import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager
import net.opendasharchive.openarchive.features.core.dialog.ResourceProvider
+import net.opendasharchive.openarchive.features.folders.BrowseFoldersViewModel
+import net.opendasharchive.openarchive.features.main.MainViewModel
+import net.opendasharchive.openarchive.features.main.ui.HomeViewModel
import org.koin.android.ext.koin.androidApplication
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
@@ -13,6 +19,18 @@ val coreModule = module {
// Provide DialogStateManager and let Koin inject the ResourceProvider.
viewModel { DialogStateManager(get()) }
+
+ viewModel { HomeViewModel() }
+
+ viewModel {
+ MainViewModel()
+ }
+
+ viewModel {
+ BrowseFoldersViewModel(
+ context = get()
+ )
+ }
}
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
index 0d743787..f15c2087 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/core/di/FeaturesModule.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/core/di/FeaturesModule.kt
@@ -27,40 +27,8 @@ val featuresModule = module {
includes(internetArchiveModule)
// TODO: have some registry of feature modules
- single {
- AppConfig(
- passcodeLength = 6,
- enableHapticFeedback = true,
- maxRetryLimitEnabled = false,
- biometricAuthEnabled = false,
- maxFailedAttempts = 5,
- snowbirdEnabled = true
- )
- }
- single {
- HapticManager(
- appConfig = get(),
- )
- }
- single {
- PBKDF2HashingStrategy()
- }
-
- single { AppConfig() }
-
- single {
- val hashingStrategy: HashingStrategy = PBKDF2HashingStrategy()
-
- PasscodeRepository(
- context = get(),
- config = get(),
- hashingStrategy = hashingStrategy
- )
- }
- viewModel { PasscodeEntryViewModel(get(), get()) }
- viewModel { PasscodeSetupViewModel(get(), get()) }
// single { SnowbirdFileRepository(get(named("retrofit"))) }
// single { SnowbirdGroupRepository(get(named("retrofit"))) }
diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/di/PasscodeModule.kt b/app/src/main/java/net/opendasharchive/openarchive/core/di/PasscodeModule.kt
new file mode 100644
index 00000000..0973bdcf
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/core/di/PasscodeModule.kt
@@ -0,0 +1,48 @@
+package net.opendasharchive.openarchive.core.di
+
+import android.content.Context
+import net.opendasharchive.openarchive.features.settings.passcode.AppConfig
+import net.opendasharchive.openarchive.features.settings.passcode.HapticManager
+import net.opendasharchive.openarchive.features.settings.passcode.HashingStrategy
+import net.opendasharchive.openarchive.features.settings.passcode.PBKDF2HashingStrategy
+import net.opendasharchive.openarchive.features.settings.passcode.PasscodeRepository
+import net.opendasharchive.openarchive.features.settings.passcode.passcode_entry.PasscodeEntryViewModel
+import net.opendasharchive.openarchive.features.settings.passcode.passcode_setup.PasscodeSetupViewModel
+import org.koin.core.module.dsl.viewModel
+import org.koin.dsl.module
+
+val passcodeModule = module {
+ single {
+ AppConfig(
+ passcodeLength = 6,
+ enableHapticFeedback = true,
+ maxRetryLimitEnabled = false,
+ biometricAuthEnabled = false,
+ maxFailedAttempts = 5,
+ isDwebEnabled = true
+ )
+ }
+
+ single {
+ HapticManager(
+ appConfig = get(),
+ )
+ }
+
+ single {
+ PBKDF2HashingStrategy()
+ }
+
+ single {
+ val hashingStrategy: HashingStrategy = PBKDF2HashingStrategy()
+
+ PasscodeRepository(
+ context = get(),
+ config = get(),
+ hashingStrategy = hashingStrategy
+ )
+ }
+
+ viewModel { PasscodeEntryViewModel(get(), get()) }
+ viewModel { PasscodeSetupViewModel(get(), get()) }
+}
\ No newline at end of file
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
index c7e2ff25..39612410 100644
--- 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
@@ -1,9 +1,51 @@
package net.opendasharchive.openarchive.core.presentation.components
-import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.opendasharchive.openarchive.core.presentation.theme.DefaultBoxPreview
@Composable
-fun PrimaryButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) =
- Button(onClick = onClick, content = content)
+fun PrimaryButton(
+ modifier: Modifier = Modifier,
+ icon: ImageVector? = null,
+ text: String,
+ onClick: () -> Unit
+) {
+ Button(
+ modifier = modifier,
+ shape = RoundedCornerShape(8f),
+ onClick = onClick
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ icon?.let {
+ Icon(imageVector = it, contentDescription = null)
+ }
+
+ Text(text)
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun PrimaryButtonPreview() {
+ DefaultBoxPreview {
+
+ PrimaryButton(
+ text = "New Folder"
+ ) { }
+ }
+}
\ No newline at end of file
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 1efc7585..a608bd4c 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
@@ -13,6 +13,7 @@ import net.opendasharchive.openarchive.R
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_darker_medium_grey = Color(0xff434343)
private val c23_medium_grey = Color(0xff696666)
private val c23_grey = Color(0xff9f9f9f)
private val c23_grey_50 = Color(0xff777979)
@@ -47,18 +48,18 @@ data class ColorTheme(
internal fun lightColorScheme() = ColorTheme(
material = lightColorScheme(
- primary = c23_teal,
+ primary = colorResource(R.color.colorPrimary),
onPrimary = Color.Black,
- primaryContainer = c23_teal,
- onPrimaryContainer = Color.Black,
+ primaryContainer = colorResource(R.color.colorPrimaryContainer),
+ onPrimaryContainer = colorResource(R.color.colorOnPrimaryContainer),
- secondary = c23_teal,
- onSecondary = Color.Black,
- secondaryContainer = c23_teal_90,
- onSecondaryContainer = Color.Black,
+ secondary = colorResource(R.color.colorSecondary),
+ onSecondary = colorResource(R.color.colorOnSecondary),
+ secondaryContainer = colorResource(R.color.colorSecondaryContainer),
+ onSecondaryContainer = colorResource(R.color.colorOnSecondaryContainer),
- tertiary = c23_powder_blue,
- onTertiary = Color.Black,
+ tertiary = colorResource(R.color.colorTertiary),
+ onTertiary = colorResource(R.color.colorSecondary),
tertiaryContainer = c23_powder_blue,
onTertiaryContainer = Color.Black,
@@ -70,10 +71,10 @@ internal fun lightColorScheme() = ColorTheme(
background = colorResource(R.color.colorBackground),
onBackground = colorResource(R.color.colorOnBackground),
- surface = c23_light_grey,
- onSurface = Color.Black,
+ surface = Color.White,
+ onSurface = colorResource(R.color.colorOnSurface),
surfaceVariant = c23_grey,
- onSurfaceVariant = c23_darker_grey,
+ onSurfaceVariant = c23_darker_medium_grey,
outline = Color.Black,
inverseOnSurface = Color.White,
@@ -92,18 +93,19 @@ internal fun lightColorScheme() = ColorTheme(
@Composable
internal fun darkColorScheme() = ColorTheme(
material = darkColorScheme(
- primary = darkPrimary,
+
+ primary = colorResource(R.color.colorPrimary),
onPrimary = Color.White,
- primaryContainer = c23_teal,
- onPrimaryContainer = Color.White,
+ primaryContainer = colorResource(R.color.colorPrimaryContainer),
+ onPrimaryContainer = colorResource(R.color.colorOnPrimaryContainer),
- secondary = c23_teal,
- onSecondary = Color.Black,
- secondaryContainer = c23_teal_20,
- onSecondaryContainer = Color.White,
+ secondary = colorResource(R.color.colorSecondary),
+ onSecondary = colorResource(R.color.colorOnSecondary),
+ secondaryContainer = colorResource(R.color.colorSecondaryContainer),
+ onSecondaryContainer = colorResource(R.color.colorOnSecondaryContainer),
- tertiary = c23_powder_blue,
- onTertiary = Color.Black,
+ tertiary = colorResource(R.color.colorTertiary),
+ onTertiary = colorResource(R.color.colorSecondary),
tertiaryContainer = c23_powder_blue,
onTertiaryContainer = Color.Black,
diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Preview.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Preview.kt
index c0261494..598566c0 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Preview.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Preview.kt
@@ -35,6 +35,26 @@ fun DefaultScaffoldPreview(
}
+@Composable
+fun DefaultEmptyScaffoldPreview(
+ content: @Composable () -> Unit
+) {
+
+ SaveAppTheme {
+
+ Scaffold { paddingValues ->
+
+ Box(
+ modifier = Modifier.Companion.padding(paddingValues),
+ contentAlignment = Alignment.Companion.Center
+ ) {
+ content()
+ }
+ }
+ }
+
+}
+
@Composable
fun DefaultBoxPreview(
content: @Composable () -> Unit
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 20c022d3..deb3a5d3 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
@@ -24,7 +24,8 @@ fun SaveAppTheme(
MaterialTheme(
colorScheme = colors.material,
content = content,
- shapes = Shapes
+ shapes = Shapes,
+ typography = Typography,
)
}
}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Type.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Type.kt
new file mode 100644
index 00000000..2306459e
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Type.kt
@@ -0,0 +1,78 @@
+package net.opendasharchive.openarchive.core.presentation.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import net.opendasharchive.openarchive.R
+
+// Define Montserrat FontFamily
+val MontserratFontFamily = FontFamily(
+ Font(R.font.montserrat_thin, FontWeight.Thin), // 100
+ Font(R.font.montserrat_extra_light, FontWeight.ExtraLight), // 200
+ Font(R.font.montserrat_light, FontWeight.Light), // 300
+ Font(R.font.montserrat_regular, FontWeight.Normal), // 400
+ Font(R.font.montserrat_medium, FontWeight.Medium), // 500
+ Font(R.font.montserrat_semi_bold, FontWeight.SemiBold), // 600
+ Font(R.font.montserrat_bold, FontWeight.Bold), // 700
+ Font(R.font.montserrat_extra_bold, FontWeight.ExtraBold), // 800
+ Font(R.font.montserrat_black, FontWeight.Black) // 900
+)
+
+// Define Montserrat Italic FontFamily
+val MontserratItalicFontFamily = FontFamily(
+ Font(R.font.montserrat_thin_italic, FontWeight.Thin), // 100
+ Font(R.font.montserrat_extra_light_italic, FontWeight.ExtraLight), // 200
+ Font(R.font.montserrat_light_italic, FontWeight.Light), // 300
+ Font(R.font.montserrat_italic, FontWeight.Normal), // 400
+ Font(R.font.montserrat_medium_italic, FontWeight.Medium), // 500
+ Font(R.font.montserrat_semi_bold_italic, FontWeight.SemiBold), // 600
+ Font(R.font.montserrat_bold_italic, FontWeight.Bold), // 700
+ Font(R.font.montserrat_extra_bold_italic, FontWeight.ExtraBold), // 800
+ Font(R.font.montserrat_black_italic, FontWeight.Black) // 900
+)
+
+
+val Typography = Typography(
+ headlineSmall = TextStyle(
+ fontFamily = MontserratFontFamily,
+ fontSize = 18.sp,
+ lineHeight = 22.sp,
+ fontWeight = FontWeight.SemiBold // 600
+ ),
+ bodyLarge = TextStyle(
+ fontFamily = MontserratFontFamily,
+ fontSize = 16.sp,
+ lineHeight = 20.sp,
+ fontWeight = FontWeight.SemiBold // 600
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = MontserratFontFamily,
+ fontSize = 14.sp,
+ lineHeight = 16.sp,
+ fontWeight = FontWeight.Medium // 500
+ ),
+ bodySmall = TextStyle(
+ fontFamily = MontserratFontFamily,
+ fontSize = 11.sp,
+ lineHeight = 14.sp,
+ fontWeight = FontWeight.Medium // 500
+ ),
+ labelMedium = TextStyle(
+ fontFamily = MontserratFontFamily,
+ fontSize = 11.sp,
+ fontWeight = FontWeight.Medium // 500
+ ),
+ titleLarge = TextStyle(
+ fontFamily = MontserratFontFamily,
+ fontSize = 22.sp, // Adjust according to UI needs
+ fontWeight = FontWeight.Normal // Default for TitleLarge
+ ),
+ titleMedium = TextStyle(
+ fontFamily = MontserratFontFamily,
+ fontSize = 18.sp, // Adjust according to UI needs
+ fontWeight = FontWeight.Medium // 500
+ )
+)
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/MediaViewHolder.kt b/app/src/main/java/net/opendasharchive/openarchive/db/MediaViewHolder.kt
deleted file mode 100644
index 036dea04..00000000
--- a/app/src/main/java/net/opendasharchive/openarchive/db/MediaViewHolder.kt
+++ /dev/null
@@ -1,429 +0,0 @@
-package net.opendasharchive.openarchive.db
-
-import android.annotation.SuppressLint
-import android.text.format.Formatter
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
-import androidx.core.content.ContextCompat
-import androidx.recyclerview.widget.RecyclerView
-import androidx.swiperefreshlayout.widget.CircularProgressDrawable
-import androidx.viewbinding.ViewBinding
-import com.bumptech.glide.Glide
-import com.github.derlio.waveform.SimpleWaveformView
-import com.github.derlio.waveform.soundfile.SoundFile
-import com.google.android.material.progressindicator.CircularProgressIndicator
-import com.squareup.picasso.Picasso
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.launch
-import net.opendasharchive.openarchive.R
-import net.opendasharchive.openarchive.core.logger.AppLogger
-import net.opendasharchive.openarchive.databinding.RvMediaBoxBinding
-import net.opendasharchive.openarchive.databinding.RvMediaRowBigBinding
-import net.opendasharchive.openarchive.databinding.RvMediaRowSmallBinding
-import net.opendasharchive.openarchive.fragments.VideoRequestHandler
-import net.opendasharchive.openarchive.util.extensions.hide
-import net.opendasharchive.openarchive.util.extensions.show
-import timber.log.Timber
-import java.io.InputStream
-
-abstract class MediaViewHolder(protected val binding: ViewBinding) :
- RecyclerView.ViewHolder(binding.root) {
-
- class Box(parent: ViewGroup) : MediaViewHolder(
- RvMediaBoxBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- ) {
- override val image: ImageView
- get() = (binding as RvMediaBoxBinding).image
-
- override val waveform: SimpleWaveformView
- get() = (binding as RvMediaBoxBinding).waveform
-
- override val videoIndicator: ImageView
- get() = (binding as RvMediaBoxBinding).videoIndicator
-
- override val overlayContainer: View
- get() = (binding as RvMediaBoxBinding).overlayContainer
-
- override val progress: CircularProgressIndicator
- get() = (binding as RvMediaBoxBinding).progress
-
- override val progressText: TextView
- get() = (binding as RvMediaBoxBinding).progressText
-
- override val error: ImageView
- get() = (binding as RvMediaBoxBinding).error
-
- override val title: TextView?
- get() = null //(binding as RvMediaBoxBinding).title
-
- override val fileInfo: TextView?
- get() = null //(binding as RvMediaBoxBinding).fileInfo
-
- override val locationIndicator: ImageView?
- get() = null
-
- override val tagsIndicator: ImageView?
- get() = null
-
- override val descIndicator: ImageView?
- get() = null
-
- override val flagIndicator: ImageView?
- get() = null
-
- override val selectedIndicator: View
- get() = (binding as RvMediaBoxBinding).selectedIndicator
-
- override val handle: ImageView?
- get() = null
- }
-
- class BigRow(parent: ViewGroup) : MediaViewHolder(
- RvMediaRowBigBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- ) {
- override val image: ImageView
- get() = (binding as RvMediaRowBigBinding).image
-
- override val waveform: SimpleWaveformView
- get() = (binding as RvMediaRowBigBinding).waveform
-
- override val videoIndicator: ImageView
- get() = (binding as RvMediaRowBigBinding).videoIndicator
-
- override val overlayContainer: View?
- get() = null
-
- override val progress: CircularProgressIndicator?
- get() = null
-
- override val progressText: TextView?
- get() = null
-
- override val error: ImageView?
- get() = null
-
- override val title: TextView
- get() = (binding as RvMediaRowBigBinding).title
-
- override val fileInfo: TextView
- get() = (binding as RvMediaRowBigBinding).fileInfo
-
- override val locationIndicator: ImageView
- get() = (binding as RvMediaRowBigBinding).locationIndicator
-
- override val tagsIndicator: ImageView
- get() = (binding as RvMediaRowBigBinding).tagsIndicator
-
- override val descIndicator: ImageView
- get() = (binding as RvMediaRowBigBinding).descIndicator
-
- override val flagIndicator: ImageView
- get() = (binding as RvMediaRowBigBinding).flagIndicator
-
- override val selectedIndicator: View?
- get() = null
-
- override val handle: ImageView?
- get() = null
- }
-
- class SmallRow(parent: ViewGroup) : MediaViewHolder(
- RvMediaRowSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- ) {
- override val image: ImageView
- get() = (binding as RvMediaRowSmallBinding).image
-
- override val waveform: SimpleWaveformView
- get() = (binding as RvMediaRowSmallBinding).waveform
-
- override val videoIndicator: ImageView?
- get() = null
-
- override val overlayContainer: View
- get() = (binding as RvMediaRowSmallBinding).overlayContainer
-
- override val progress: CircularProgressIndicator
- get() = (binding as RvMediaRowSmallBinding).progress
-
- override val progressText: TextView
- get() = (binding as RvMediaRowSmallBinding).progressText
-
- override val error: ImageView
- get() = (binding as RvMediaRowSmallBinding).error
-
- override val title: TextView
- get() = (binding as RvMediaRowSmallBinding).title
-
- override val fileInfo: TextView
- get() = (binding as RvMediaRowSmallBinding).fileInfo
-
- override val locationIndicator: ImageView?
- get() = null
-
- override val tagsIndicator: ImageView?
- get() = null
-
- override val descIndicator: ImageView?
- get() = null
-
- override val flagIndicator: ImageView?
- get() = null
-
- override val selectedIndicator: View?
- get() = null
-
- override val handle: ImageView
- get() = (binding as RvMediaRowSmallBinding).handle
- }
-
-
- companion object {
- val soundCache = HashMap()
- }
-
-
- abstract val image: ImageView
- abstract val waveform: SimpleWaveformView
- abstract val videoIndicator: ImageView?
- abstract val overlayContainer: View?
- abstract val progress: CircularProgressIndicator?
- abstract val progressText: TextView?
- abstract val error: ImageView?
- abstract val title: TextView?
- abstract val fileInfo: TextView?
- abstract val locationIndicator: ImageView?
- abstract val tagsIndicator: ImageView?
- abstract val descIndicator: ImageView?
- abstract val flagIndicator: ImageView?
- abstract val selectedIndicator: View?
- abstract val handle: ImageView?
-
- private val mContext = itemView.context
-
- private val mPicasso = Picasso.Builder(mContext)
- .addRequestHandler(VideoRequestHandler(mContext))
- .build()
-
-
- @SuppressLint("SetTextI18n")
- fun bind(media: Media? = null, batchMode: Boolean = false, doImageFade: Boolean = true) {
- AppLogger.i("Binding media item ${media?.id} with status ${media?.sStatus} and progress ${media?.uploadPercentage}")
- itemView.tag = media?.id
-
- if (batchMode && media?.selected == true) {
- itemView.setBackgroundResource(R.color.colorPrimary)
- selectedIndicator?.show()
- } else {
- itemView.setBackgroundResource(R.color.transparent)
- selectedIndicator?.hide()
- }
-
- image.alpha = if (media?.sStatus == Media.Status.Uploaded || !doImageFade) 1f else 0.5f
-
- if (media?.mimeType?.startsWith("image") == true) {
- val progress = CircularProgressDrawable(mContext)
- progress.strokeWidth = 5f
- progress.centerRadius = 30f
- progress.start()
-
- Glide.with(mContext)
- .load(media.fileUri)
- .placeholder(progress)
- .fitCenter()
- .into(image)
-
- image.show()
- waveform.hide()
- videoIndicator?.hide()
- } else if (media?.mimeType?.startsWith("video") == true) {
- mPicasso.load(VideoRequestHandler.SCHEME_VIDEO + ":" + media.originalFilePath)
- .fit()
- .centerCrop()
- .into(image)
-
- image.show()
- waveform.hide()
- videoIndicator?.show()
- } else if (media?.mimeType?.startsWith("audio") == true) {
- videoIndicator?.hide()
-
- val soundFile = soundCache[media.originalFilePath]
-
- if (soundFile != null) {
- image.hide()
- waveform.setAudioFile(soundFile)
- waveform.show()
- } else {
- image.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.no_thumbnail))
- image.show()
- waveform.hide()
-
- CoroutineScope(Dispatchers.IO).launch {
- @Suppress("NAME_SHADOWING")
- val soundFile = try {
- SoundFile.create(media.originalFilePath) {
- return@create true
- }
- } catch (e: Throwable) {
- Timber.d(e)
-
- null
- }
-
- if (soundFile != null) {
- soundCache[media.originalFilePath] = soundFile
-
- MainScope().launch {
- waveform.setAudioFile(soundFile)
- image.hide()
- waveform.show()
- }
- }
- }
- }
- } else {
- image.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.no_thumbnail))
- image.show()
- waveform.hide()
- videoIndicator?.hide()
- }
-
- if (media != null) {
- val file = media.file
-
- if (file.exists()) {
- fileInfo?.text = Formatter.formatShortFileSize(mContext, file.length())
- } else {
- if (media.contentLength == -1L) {
- var iStream: InputStream? = null
- try {
- iStream = mContext.contentResolver.openInputStream(media.fileUri)
-
- if (iStream != null) {
- media.contentLength = iStream.available().toLong()
- media.save()
- }
- } catch (e: Throwable) {
- Timber.e(e)
- } finally {
- iStream?.close()
- }
- }
-
- fileInfo?.text = if (media.contentLength > 0) {
- Formatter.formatShortFileSize(mContext, media.contentLength)
- } else {
- media.formattedCreateDate
- }
- }
-
- fileInfo?.show()
- } else {
- fileInfo?.hide()
- }
-
- val sbTitle = StringBuffer()
-
- if (media?.sStatus == Media.Status.Error) {
- AppLogger.i("Media Item ${media.id} is error")
- sbTitle.append(mContext.getString(R.string.error))
-
- overlayContainer?.show()
- progress?.hide()
- progressText?.hide()
- error?.show()
-
- if (media.statusMessage.isNotBlank()) {
- fileInfo?.text = media.statusMessage
- fileInfo?.show()
- }
- } else if (media?.sStatus == Media.Status.Queued) {
- AppLogger.i("Media Item ${media.id} is queued")
- overlayContainer?.show()
- progress?.isIndeterminate = true
- progress?.show()
- progressText?.hide()
- error?.hide()
- } else if (media?.sStatus == Media.Status.Uploading) {
-// val progressValue = if (media.contentLength > 0) {
-// (media.progress.toFloat() / media.contentLength.toFloat() * 100f).roundToInt()
-// } else 0
- progress?.isIndeterminate = false
- val progressValue = media.uploadPercentage ?: 0
- AppLogger.i("Media Item ${media.id} is uploading")
-
- overlayContainer?.show()
- progress?.show()
- progressText?.show()
-
- // Make sure to keep spinning until the upload has made some noteworthy progress.
- if (progressValue > 2) {
- progress?.setProgressCompat(progressValue, true)
- }
-// else {
-// progress?.isIndeterminate = true
-// }
-
- progressText?.text = "${progressValue}%"
-
- error?.hide()
- } else {
- overlayContainer?.hide()
- progress?.hide()
- progressText?.hide()
- error?.hide()
- }
-
- if (sbTitle.isNotEmpty()) sbTitle.append(": ")
- sbTitle.append(media?.title)
-
- if (sbTitle.isNotBlank()) {
- title?.text = sbTitle.toString()
- title?.show()
- } else {
- title?.hide()
- }
-
- locationIndicator?.setImageResource(
- if (media?.location.isNullOrBlank()) R.drawable.ic_location_unselected
- else R.drawable.ic_location_selected
- )
-
- tagsIndicator?.setImageResource(
- if (media?.tags.isNullOrBlank()) R.drawable.ic_tag_unselected
- else R.drawable.ic_tag_selected
- )
-
- descIndicator?.setImageResource(
- if (media?.description.isNullOrBlank()) R.drawable.ic_edit_unselected
- else R.drawable.ic_edit_selected
- )
-
- flagIndicator?.setImageResource(
- if (media?.flag == true) R.drawable.ic_flag_selected
- else R.drawable.ic_flag_unselected
- )
- }
-
- fun updateProgress(progressValue: Int) {
- if (progressValue > 2) {
- progress?.isIndeterminate = false
- progress?.setProgressCompat(progressValue, true)
- } else {
- progress?.isIndeterminate = true
- }
-
- AppLogger.i("Updating progressText to $progressValue%")
- if (progressText == null) {
- AppLogger.e("progressText is null")
- } else {
- progressText?.show(animate = true)
- progressText?.text = "$progressValue%"
- }
- }
-}
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 f9439ccb..562c287f 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt
@@ -6,10 +6,17 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
import androidx.core.content.ContextCompat
import com.github.abdularis.civ.AvatarImageView
import com.orm.SugarRecord
import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.logger.AppLogger
import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity
import net.opendasharchive.openarchive.services.gdrive.GDriveConduit
import net.opendasharchive.openarchive.services.internetarchive.IaConduit
@@ -53,6 +60,7 @@ data class Space(
name = IaConduit.NAME
host = IaConduit.ARCHIVE_API_ENDPOINT
}
+
Type.GDRIVE -> {
name = GDriveConduit.NAME
}
@@ -62,10 +70,10 @@ data class Space(
}
enum class Type(val id: Int, val friendlyName: String) {
- WEBDAV(0, "WebDAV"),
+ WEBDAV(0, "Private Server"),
INTERNET_ARCHIVE(1, IaConduit.NAME),
GDRIVE(4, GDriveConduit.NAME),
- RAVEN(5, "Raven"),
+ RAVEN(5, "DWeb Service"),
}
enum class IconStyle {
@@ -91,8 +99,10 @@ data class Space(
whereArgs.add(username)
}
- return find(Space::class.java, whereClause, whereArgs.toTypedArray(),
- null, null, null)
+ return find(
+ Space::class.java, whereClause, whereArgs.toTypedArray(),
+ null, null, null
+ )
}
fun has(type: Type, host: String? = null, username: String? = null): Boolean {
@@ -100,8 +110,12 @@ data class Space(
}
var current: Space?
- get() = get(Prefs.currentSpaceId) ?: first(Space::class.java)
+ get() {
+ AppLogger.i("getting current space....")
+ return get(Prefs.currentSpaceId) ?: first(Space::class.java)
+ }
set(value) {
+ AppLogger.i("setting current space... ${value?.displayname}")
Prefs.currentSpaceId = value?.id ?: -1
}
@@ -112,8 +126,7 @@ data class Space(
fun navigate(activity: AppCompatActivity) {
if (getAll().hasNext()) {
activity.finish()
- }
- else {
+ } else {
activity.finishAffinity()
activity.startActivity(Intent(activity, SpaceSetupActivity::class.java))
}
@@ -135,8 +148,8 @@ data class Space(
val hostUrl: HttpUrl?
get() = host.toHttpUrlOrNull()
- var tType: Type?
- get() = Type.values().firstOrNull { it.id == type }
+ var tType: Type
+ get() = Type.entries.first { it.id == type }
set(value) {
type = (value ?: Type.WEBDAV).id
}
@@ -160,30 +173,85 @@ data class Space(
// }
val projects: List
- get() = find(Project::class.java, "space_id = ? AND NOT archived", arrayOf(id.toString()), null, "id DESC", null)
+ get() = find(
+ Project::class.java,
+ "space_id = ? AND NOT archived",
+ arrayOf(id.toString()),
+ null,
+ "id DESC",
+ null
+ )
val archivedProjects: List
- get() = find(Project::class.java, "space_id = ? AND archived", arrayOf(id.toString()), null, "id DESC", null)
+ get() = find(
+ Project::class.java,
+ "space_id = ? AND archived",
+ arrayOf(id.toString()),
+ null,
+ "id DESC",
+ null
+ )
fun hasProject(description: String): Boolean {
// Cannot use `count` from Kotlin due to strange in method signature.
- return find(Project::class.java, "space_id = ? AND description = ?", id.toString(), description).size > 0
+ return find(
+ Project::class.java,
+ "space_id = ? AND description = ?",
+ id.toString(),
+ description
+ ).size > 0
}
fun getAvatar(context: Context, style: IconStyle = IconStyle.SOLID): Drawable? {
- val color = ContextCompat.getColor(context, R.color.colorOnBackground)
+
return when (tType) {
- Type.WEBDAV -> ContextCompat.getDrawable(context, R.drawable.ic_private_server) // ?.tint(color)
+ Type.WEBDAV -> ContextCompat.getDrawable(
+ context,
+ R.drawable.ic_private_server
+ ) // ?.tint(color)
- Type.INTERNET_ARCHIVE -> ContextCompat.getDrawable(context, R.drawable.ic_internet_archive) // ?.tint(color)
+ Type.INTERNET_ARCHIVE -> ContextCompat.getDrawable(
+ context,
+ R.drawable.ic_internet_archive
+ ) // ?.tint(color)
- Type.GDRIVE -> ContextCompat.getDrawable(context, R.drawable.logo_gdrive_outline) // ?.tint(color)
+ Type.GDRIVE -> ContextCompat.getDrawable(
+ context,
+ R.drawable.logo_gdrive_outline
+ ) // ?.tint(color)
Type.RAVEN -> ContextCompat.getDrawable(context, R.drawable.snowbird) // ?.tint(color)
- else -> BitmapDrawable(context.resources, DrawableUtil.createCircularTextDrawable(initial, color))
+ else -> {
+ val color = ContextCompat.getColor(context, R.color.colorOnBackground)
+ BitmapDrawable(
+ context.resources,
+ DrawableUtil.createCircularTextDrawable(initial, color)
+ )
+ }
+
+ }
+ }
+
+ @Composable
+ fun getAvatar(): Painter {
+
+ return when (tType) {
+ Type.WEBDAV -> painterResource(R.drawable.ic_space_private_server)
+
+ Type.INTERNET_ARCHIVE -> painterResource(R.drawable.ic_space_interent_archive)
+
+ Type.GDRIVE -> painterResource(R.drawable.logo_gdrive_outline)
+ Type.RAVEN -> painterResource(R.drawable.ic_space_dweb)
+ null -> {
+ val context = LocalContext.current
+ val color = ContextCompat.getColor(context, R.color.colorOnBackground)
+ val bitmap = DrawableUtil.createCircularTextDrawable(initial, color)
+ val imageBitmap = bitmap.asImageBitmap()
+ BitmapPainter(imageBitmap)
+ }
}
}
@@ -201,9 +269,9 @@ data class Space(
if (view is AvatarImageView) {
view.state = AvatarImageView.SHOW_INITIAL
view.setText(initial)
- view.avatarBackgroundColor = ContextCompat.getColor(view.context, R.color.colorPrimary)
- }
- else {
+ view.avatarBackgroundColor =
+ ContextCompat.getColor(view.context, R.color.colorPrimary)
+ } else {
view.setImageDrawable(getAvatar(view.context))
}
}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/MediaAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/db/UploadMediaAdapter.kt
similarity index 58%
rename from app/src/main/java/net/opendasharchive/openarchive/db/MediaAdapter.kt
rename to app/src/main/java/net/opendasharchive/openarchive/db/UploadMediaAdapter.kt
index 4d1b82fd..54cedf8f 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/MediaAdapter.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/UploadMediaAdapter.kt
@@ -1,8 +1,7 @@
package net.opendasharchive.openarchive.db
-import android.annotation.SuppressLint
import android.app.Activity
-import android.content.Intent
+import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
@@ -10,37 +9,26 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import net.opendasharchive.openarchive.R
import net.opendasharchive.openarchive.core.logger.AppLogger
-import net.opendasharchive.openarchive.features.media.PreviewActivity
+import net.opendasharchive.openarchive.databinding.RvMediaRowSmallBinding
import net.opendasharchive.openarchive.upload.BroadcastManager
-import net.opendasharchive.openarchive.upload.UploadManagerActivity
import net.opendasharchive.openarchive.upload.UploadService
import net.opendasharchive.openarchive.util.AlertHelper
import net.opendasharchive.openarchive.util.Prefs
-import net.opendasharchive.openarchive.util.extensions.toggle
import java.lang.ref.WeakReference
-class MediaAdapter(
+class UploadMediaAdapter(
activity: Activity?,
- private val generator: (parent: ViewGroup) -> MediaViewHolder,
- data: List,
+ mediaItems: List,
private val recyclerView: RecyclerView,
- private val supportedStatuses: List = listOf(
- Media.Status.Local,
- Media.Status.Uploading,
- Media.Status.Error
- ),
private val checkSelecting: (() -> Unit)? = null
-) : RecyclerView.Adapter() {
+) : RecyclerView.Adapter() {
- var media: ArrayList = ArrayList(data)
+ var media: ArrayList = ArrayList(mediaItems)
private set
var doImageFade = true
- var isEditMode = false
-
- var selecting = false
- private set
+ var isEditMode = true
private var mActivity = WeakReference(activity)
@@ -49,68 +37,47 @@ class MediaAdapter(
}
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
- val mvh = generator(parent)
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UploadMediaViewHolder {
+ val binding =
+ RvMediaRowSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ val mvh = UploadMediaViewHolder(
+ binding = binding,
+ onDeleteClick = { position ->
+ deleteItem(position)
+ }
+ )
mvh.itemView.setOnClickListener { v ->
- if (selecting && checkSelecting != null) {
- selectView(v)
+ val pos = recyclerView.getChildLayoutPosition(v)
+ val mediaItem = media[pos]
+
+ if (mediaItem.sStatus == Media.Status.Error) {
+ mActivity.get()?.let {
+ AlertHelper.show(
+ it, it.getString(R.string.upload_unsuccessful_description),
+ R.string.upload_unsuccessful, R.drawable.ic_error, listOf(
+ AlertHelper.positiveButton(R.string.retry) { _, _ ->
+
+ media[pos].apply {
+ sStatus = Media.Status.Queued
+ statusMessage = ""
+ save()
+
+ BroadcastManager.postChange(it, collectionId, id)
+ }
+
+ UploadService.startUploadService(it)
+ },
+ AlertHelper.negativeButton(R.string.remove) { _, _ ->
+ deleteItem(pos)
+ },
+ AlertHelper.neutralButton()
+ )
+ )
+ }
} else {
- val pos = recyclerView.getChildLayoutPosition(v)
-
- when (media[pos].sStatus) {
- Media.Status.Local -> {
- if (supportedStatuses.contains(Media.Status.Local)) {
- mActivity.get()?.let {
- PreviewActivity.start(it, media[pos].projectId)
- }
- }
- }
-
- Media.Status.Queued, Media.Status.Uploading -> {
- if (supportedStatuses.contains(Media.Status.Uploading)) {
- mActivity.get()?.let {
- it.startActivity(
- Intent(it, UploadManagerActivity::class.java)
- )
- }
- }
- }
-
- Media.Status.Error -> {
- if (supportedStatuses.contains(Media.Status.Error)) {
- //CleanInsightsManager.measureEvent("backend", "upload-error", media[pos].space?.friendlyName)
- mActivity.get()?.let {
- AlertHelper.show(
- it, it.getString(R.string.upload_unsuccessful_description),
- R.string.upload_unsuccessful, R.drawable.ic_error, listOf(
- AlertHelper.positiveButton(R.string.retry) { _, _ ->
-
- media[pos].apply {
- sStatus = Media.Status.Queued
- statusMessage = ""
- save()
-
- BroadcastManager.postChange(it, collectionId, id)
- }
-
- UploadService.startUploadService(it)
- },
- AlertHelper.negativeButton(R.string.remove) { _, _ ->
- deleteItem(pos)
- },
- AlertHelper.neutralButton()
- )
- )
- }
- }
- }
-
- else -> {
- if (checkSelecting != null) {
- selectView(v)
- }
- }
+ if (checkSelecting != null) {
+ selectView(v)
}
}
}
@@ -123,19 +90,6 @@ class MediaAdapter(
}
}
- mvh.flagIndicator?.setOnClickListener {
- showFirstTimeFlag()
-
- // Toggle flag
- val mediaId = mvh.itemView.tag as? Long ?: return@setOnClickListener
-
- val item = media.firstOrNull { it.id == mediaId } ?: return@setOnClickListener
- item.flag = !item.flag
- item.save()
-
- notifyItemChanged(media.indexOf(item))
- }
-
return mvh
}
@@ -145,28 +99,32 @@ class MediaAdapter(
return media[position].id
}
- @SuppressLint("ClickableViewAccessibility")
- override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
+ override fun onBindViewHolder(holder: UploadMediaViewHolder, position: Int) {
AppLogger.i("onBindViewHolder called for position $position")
- holder.bind(media[position], selecting, doImageFade)
- holder.handle?.toggle(isEditMode)
+ holder.bind(media[position], doImageFade)
+ holder.toggleEditMode(isEditMode)
}
- override fun onBindViewHolder(holder: MediaViewHolder, position: Int, payloads: MutableList) {
+ override fun onBindViewHolder(
+ holder: UploadMediaViewHolder,
+ position: Int,
+ payloads: MutableList
+ ) {
if (payloads.isNotEmpty()) {
val payload = payloads[0]
when (payload) {
"progress" -> {
holder.updateProgress(media[position].uploadPercentage ?: 0)
}
+
"full" -> {
- holder.bind(media[position], selecting, doImageFade)
- holder.handle?.toggle(isEditMode)
+ holder.bind(media[position], doImageFade)
+ holder.toggleEditMode(isEditMode)
}
}
} else {
- holder.bind(media[position], selecting, doImageFade)
- holder.handle?.toggle(isEditMode)
+ holder.bind(media[position], doImageFade)
+ holder.toggleEditMode(isEditMode)
}
}
@@ -231,7 +189,6 @@ class MediaAdapter(
notifyItemChanged(media.indexOf(m))
- selecting = media.firstOrNull { it.selected } != null
checkSelecting?.invoke()
}
@@ -310,8 +267,6 @@ class MediaAdapter(
hasDeleted = true
}
- selecting = false
-
checkSelecting?.invoke()
return hasDeleted
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/UploadMediaViewHolder.kt b/app/src/main/java/net/opendasharchive/openarchive/db/UploadMediaViewHolder.kt
new file mode 100644
index 00000000..4caa01fd
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/UploadMediaViewHolder.kt
@@ -0,0 +1,247 @@
+package net.opendasharchive.openarchive.db
+
+import android.text.format.Formatter
+import android.widget.ImageView
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import androidx.swiperefreshlayout.widget.CircularProgressDrawable
+import com.bumptech.glide.Glide
+import com.github.derlio.waveform.soundfile.SoundFile
+import com.squareup.picasso.Picasso
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.logger.AppLogger
+import net.opendasharchive.openarchive.databinding.RvMediaRowSmallBinding
+import net.opendasharchive.openarchive.fragments.VideoRequestHandler
+import net.opendasharchive.openarchive.util.extensions.hide
+import net.opendasharchive.openarchive.util.extensions.show
+import net.opendasharchive.openarchive.util.extensions.toggle
+import timber.log.Timber
+import java.io.InputStream
+
+class UploadMediaViewHolder(
+ private val binding: RvMediaRowSmallBinding,
+ private val onDeleteClick: (Int) -> Unit
+) : RecyclerView.ViewHolder(binding.root) {
+
+
+ companion object {
+ val soundCache = HashMap()
+ }
+
+
+ private val mContext = itemView.context
+
+ private val mPicasso = Picasso.Builder(mContext)
+ .addRequestHandler(VideoRequestHandler(mContext))
+ .build()
+
+ init {
+ binding.btnDelete.setOnClickListener {
+ val position = bindingAdapterPosition
+ if (position != RecyclerView.NO_POSITION) {
+ onDeleteClick(position)
+ }
+ }
+ }
+
+ fun bind(media: Media? = null, doImageFade: Boolean = true) {
+ AppLogger.i("Binding media item ${media?.id} with status ${media?.sStatus} and progress ${media?.uploadPercentage}")
+ itemView.tag = media?.id
+
+ binding.image.alpha =
+ if (media?.sStatus == Media.Status.Uploaded || !doImageFade) 1f else 0.5f
+
+ if (media?.mimeType?.startsWith("image") == true) {
+ val progress = CircularProgressDrawable(mContext)
+ progress.strokeWidth = 5f
+ progress.centerRadius = 30f
+ progress.start()
+
+ Glide.with(mContext)
+ .load(media.fileUri)
+ .placeholder(progress)
+ .fitCenter()
+ .into(binding.image)
+ binding.image.scaleType = ImageView.ScaleType.CENTER_CROP
+ binding.image.show()
+ binding.waveform.hide()
+ } else if (media?.mimeType?.startsWith("video") == true) {
+ mPicasso.load(VideoRequestHandler.SCHEME_VIDEO + ":" + media.originalFilePath)
+ .fit()
+ .centerCrop()
+ .into(binding.image)
+ binding.image.scaleType = ImageView.ScaleType.CENTER_CROP
+ binding.image.show()
+ binding.waveform.hide()
+ } else if (media?.mimeType?.startsWith("audio") == true) {
+
+ val soundFile = soundCache[media.originalFilePath]
+
+ if (soundFile != null) {
+ binding.image.hide()
+ binding.waveform.setAudioFile(soundFile)
+ binding.waveform.show()
+ } else {
+ binding.image.setImageDrawable(
+ ContextCompat.getDrawable(
+ mContext,
+ R.drawable.no_thumbnail
+ )
+ )
+ binding.image.scaleType = ImageView.ScaleType.CENTER_CROP
+ binding.image.show()
+ binding.waveform.hide()
+
+ CoroutineScope(Dispatchers.IO).launch {
+ @Suppress("NAME_SHADOWING")
+ val soundFile = try {
+ SoundFile.create(media.originalFilePath) {
+ return@create true
+ }
+ } catch (e: Throwable) {
+ Timber.d(e)
+
+ null
+ }
+
+ if (soundFile != null) {
+ soundCache[media.originalFilePath] = soundFile
+
+ MainScope().launch {
+ binding.waveform.setAudioFile(soundFile)
+ binding.image.hide()
+ binding.waveform.show()
+ }
+ }
+ }
+ }
+ } else {
+ binding.image.setImageDrawable(
+ ContextCompat.getDrawable(
+ mContext,
+ R.drawable.ic_unknown_file
+ )
+ )
+ binding.image.scaleType = ImageView.ScaleType.CENTER_INSIDE
+ binding.image.show()
+ binding.waveform.hide()
+ }
+
+ if (media != null) {
+ val file = media.file
+
+ if (file.exists()) {
+ binding.fileInfo.text = Formatter.formatShortFileSize(mContext, file.length())
+ } else {
+ if (media.contentLength == -1L) {
+ var iStream: InputStream? = null
+ try {
+ iStream = mContext.contentResolver.openInputStream(media.fileUri)
+
+ if (iStream != null) {
+ media.contentLength = iStream.available().toLong()
+ media.save()
+ }
+ } catch (e: Throwable) {
+ Timber.e(e)
+ } finally {
+ iStream?.close()
+ }
+ }
+
+ binding.fileInfo.text = if (media.contentLength > 0) {
+ Formatter.formatShortFileSize(mContext, media.contentLength)
+ } else {
+ media.formattedCreateDate
+ }
+ }
+
+ binding.fileInfo.show()
+ } else {
+ binding.fileInfo.hide()
+ }
+
+ val sbTitle = StringBuffer()
+
+ if (media?.sStatus == Media.Status.Error) {
+ AppLogger.i("Media Item ${media.id} is error")
+ sbTitle.append(mContext.getString(R.string.error))
+
+ binding.overlayContainer.show()
+ binding.progress.hide()
+ binding.progressText.hide()
+ binding.error.show()
+
+ if (media.statusMessage.isNotBlank()) {
+ binding.fileInfo.text = media.statusMessage
+ binding.fileInfo.show()
+ }
+ } else if (media?.sStatus == Media.Status.Queued) {
+ AppLogger.i("Media Item ${media.id} is queued")
+ binding.overlayContainer.show()
+ binding.progress.isIndeterminate = true
+ binding.progress.show()
+ binding.progressText.hide()
+ binding.error.hide()
+ } else if (media?.sStatus == Media.Status.Uploading) {
+// val progressValue = if (media.contentLength > 0) {
+// (media.progress.toFloat() / media.contentLength.toFloat() * 100f).roundToInt()
+// } else 0
+ binding.progress.isIndeterminate = false
+ val progressValue = media.uploadPercentage ?: 0
+ AppLogger.i("Media Item ${media.id} is uploading")
+
+ binding.overlayContainer.show()
+ binding.progress.show()
+ binding.progressText.show()
+
+ // Make sure to keep spinning until the upload has made some noteworthy progress.
+ if (progressValue > 2) {
+ binding.progress.setProgressCompat(progressValue, true)
+ }
+// else {
+// progress?.isIndeterminate = true
+// }
+
+ binding.progressText.text = "${progressValue}%"
+
+ binding.error.hide()
+ } else {
+ binding.overlayContainer.hide()
+ binding.progress.hide()
+ binding.progressText.hide()
+ binding.error.hide()
+ }
+
+ if (sbTitle.isNotEmpty()) sbTitle.append(": ")
+ sbTitle.append(media?.title)
+
+ if (sbTitle.isNotBlank()) {
+ binding.title.text = sbTitle.toString()
+ binding.title.show()
+ } else {
+ binding.title.hide()
+ }
+ }
+
+ fun updateProgress(progressValue: Int) {
+ if (progressValue > 2) {
+ binding.progress.isIndeterminate = false
+ binding.progress.setProgressCompat(progressValue, true)
+ } else {
+ binding.progress.isIndeterminate = true
+ }
+
+ binding.progressText.show(animate = true)
+ binding.progressText.text = "$progressValue%"
+ }
+
+ fun toggleEditMode(isEdit: Boolean) {
+ binding.handle.toggle(isEdit)
+ binding.btnDelete.toggle(isEdit)
+ }
+}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/extensions/BottomSheetExtensions.kt b/app/src/main/java/net/opendasharchive/openarchive/extensions/BottomSheetExtensions.kt
new file mode 100644
index 00000000..6a9f4f18
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/extensions/BottomSheetExtensions.kt
@@ -0,0 +1,57 @@
+package net.opendasharchive.openarchive.extensions
+
+import android.annotation.SuppressLint
+import android.content.res.Resources
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.IdRes
+import androidx.annotation.LayoutRes
+import androidx.fragment.app.Fragment
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import net.opendasharchive.openarchive.R
+
+fun Fragment.showBottomSheetDialog(
+ @LayoutRes layout: Int,
+ @IdRes textViewToSet: Int? = null,
+ textToSet: String? = null,
+ fullScreen: Boolean = true,
+ expand: Boolean = true
+) {
+ val dialog = BottomSheetDialog(context!!)
+ dialog.setOnShowListener {
+ val bottomSheet: FrameLayout = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) ?: return@setOnShowListener
+ val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
+ if (fullScreen && bottomSheet.layoutParams != null) { showFullScreenBottomSheet(bottomSheet) }
+
+ if (!expand) return@setOnShowListener
+
+ bottomSheet.setBackgroundResource(android.R.color.transparent)
+ expandBottomSheet(bottomSheetBehavior)
+ }
+
+ @SuppressLint("InflateParams") // dialog does not need a root view here
+ val sheetView = layoutInflater.inflate(layout, null)
+ textViewToSet?.also {
+ sheetView.findViewById(it).text = textToSet
+ }
+
+// sheetView.findViewById(R.id.closeButton)?.setOnClickListener {
+// dialog.dismiss()
+// }
+
+ dialog.setContentView(sheetView)
+ dialog.show()
+}
+
+private fun showFullScreenBottomSheet(bottomSheet: FrameLayout) {
+ val layoutParams = bottomSheet.layoutParams
+ layoutParams.height = Resources.getSystem().displayMetrics.heightPixels
+ bottomSheet.layoutParams = layoutParams
+}
+
+private fun expandBottomSheet(bottomSheetBehavior: BottomSheetBehavior) {
+ bottomSheetBehavior.skipCollapsed = true
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/core/Accordion.kt b/app/src/main/java/net/opendasharchive/openarchive/features/core/Accordion.kt
new file mode 100644
index 00000000..3037a3da
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/core/Accordion.kt
@@ -0,0 +1,171 @@
+package net.opendasharchive.openarchive.features.core
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.EnterExitState
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.ripple
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+
+@Composable
+fun Accordion(
+ modifier: Modifier = Modifier,
+ headerModifier: Modifier = Modifier,
+ state: AccordionState = rememberAccordionState(),
+ animate: Boolean = true,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ headerContent: @Composable () -> Unit,
+ bodyContent: @Composable () -> Unit,
+) {
+ val expanded = state.expanded
+
+ val clickableModifier =
+ if (state.clickable) {
+ Modifier.clickable(
+ enabled = state.enabled,
+ interactionSource = interactionSource,
+ indication = ripple(),
+ onClick = { state.toggle() },
+ )
+ } else {
+ Modifier
+ }
+
+ Column(modifier = modifier) {
+ Box(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .semantics {
+ role = Role.Button
+ stateDescription = if (expanded) "Expanded" else "Collapsed"
+ }
+ .then(headerModifier)
+ .then(clickableModifier),
+ ) {
+ headerContent()
+ }
+
+ if (animate) {
+ AnimatedVisibility(
+ visible = expanded,
+ enter = expandVertically(),
+ exit = shrinkVertically(),
+ ) {
+ val progress by transition.animateFloat(label = "accordion transition") { state ->
+ if (state == EnterExitState.Visible) 1f else 0f
+ }
+
+ state.updateProgress(progress)
+
+ bodyContent()
+ }
+ } else {
+ if (expanded) {
+ bodyContent()
+ }
+ }
+ }
+}
+
+@Composable
+fun rememberAccordionState(
+ expanded: Boolean = false,
+ enabled: Boolean = true,
+ clickable: Boolean = true,
+ onExpandedChange: ((Boolean) -> Unit)? = null,
+) = remember {
+ AccordionState(expanded, enabled, clickable, onExpandedChange)
+}
+
+class AccordionState(
+ expanded: Boolean = false,
+ var enabled: Boolean = true,
+ var clickable: Boolean = true,
+ var onExpandedChange: ((Boolean) -> Unit)? = null,
+) {
+ var expanded by mutableStateOf(expanded)
+ private set
+
+ var animationProgress by mutableFloatStateOf(0f)
+ private set
+
+ fun toggle() {
+ if (!enabled) return
+ expanded = !expanded
+ onExpandedChange?.invoke(expanded)
+ }
+
+ fun updateProgress(progress: Float) {
+ animationProgress = progress
+ }
+
+ fun collapse() {
+ expanded = false
+ }
+}
+
+@Composable
+fun rememberAccordionGroupState(
+ count: Int,
+ allowMultipleOpen: Boolean = false,
+): AccordionGroupState {
+ return remember { AccordionGroupState(count, allowMultipleOpen) }
+}
+
+class AccordionGroupState(
+ count: Int,
+ private val allowMultipleOpen: Boolean,
+) {
+ private val states = List(count) { AccordionState() }
+ private var openedIndex by mutableIntStateOf(-1)
+
+ fun getState(index: Int): AccordionState {
+ val state = states[index]
+ state.onExpandedChange = { isExpanded ->
+ if (allowMultipleOpen) {
+ if (!isExpanded && openedIndex == index) {
+ openedIndex = -1
+ }
+ } else {
+ if (isExpanded) {
+ openedIndex = index
+ states.forEachIndexed { i, otherState ->
+ if (i != index) otherState.collapse()
+ }
+ } else if (openedIndex == index) {
+ openedIndex = -1
+ }
+ }
+ }
+ return state
+ }
+
+ fun collapseAll() {
+ states.forEach { it.collapse() }
+ openedIndex = -1
+ }
+
+ fun expand(index: Int) {
+ if (index in states.indices) {
+ states[index].toggle()
+ }
+ }
+}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt
index 7bef2270..260e885c 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt
@@ -103,6 +103,7 @@ abstract class BaseActivity : AppCompatActivity() {
if (showBackButton) {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back_ios)
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
} else {
supportActionBar?.setDisplayHomeAsUpEnabled(false)
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseButton.kt b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseButton.kt
index 869ef25f..2da4b992 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseButton.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseButton.kt
@@ -40,7 +40,7 @@ fun BaseButton(
contentColor = textColor
),
shape = RoundedCornerShape(cornerRadius),
- contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
+ contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp)
) {
ButtonText(text, color = textColor)
}
@@ -94,7 +94,7 @@ fun ButtonText(
text: String,
modifier: Modifier = Modifier,
fontSize: TextUnit = 16.sp,
- fontWeight: FontWeight = FontWeight.Normal,
+ fontWeight: FontWeight = FontWeight.SemiBold,
color: Color = MaterialTheme.colorScheme.onPrimary
) {
Text(
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseComposeActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseComposeActivity.kt
new file mode 100644
index 00000000..ad69572e
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseComposeActivity.kt
@@ -0,0 +1,56 @@
+package net.opendasharchive.openarchive.features.core
+
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.ui.platform.ComposeView
+import com.google.android.material.appbar.MaterialToolbar
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme
+import net.opendasharchive.openarchive.features.core.dialog.DialogHost
+import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager
+import net.opendasharchive.openarchive.util.Prefs
+import org.koin.androidx.compose.koinViewModel
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+abstract class BaseComposeActivity : AppCompatActivity() {
+
+ val dialogManager: DialogStateManager by viewModel()
+
+ companion object {
+ const val EXTRA_DATA_SPACE = "space"
+ }
+
+
+
+ override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
+ if (event != null) {
+ val obscuredTouch = event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0
+ if (obscuredTouch) return false
+ }
+
+ return super.dispatchTouchEvent(event)
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // updating this in onResume (previously was in onCreate) to make sure setting changes get
+ // applied instantly instead after the next app restart
+ updateScreenshotPrevention()
+ }
+
+ fun updateScreenshotPrevention() {
+ if (Prefs.passcodeEnabled || Prefs.prohibitScreenshots) {
+ // Prevent screenshots and recent apps preview
+ window.setFlags(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.FLAG_SECURE
+ )
+ } else {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseFragment.kt
index 7cd3423b..d0949996 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseFragment.kt
@@ -6,6 +6,7 @@ import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import net.opendasharchive.openarchive.R
import net.opendasharchive.openarchive.db.SnowbirdError
import net.opendasharchive.openarchive.extensions.androidViewModel
import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager
@@ -22,6 +23,12 @@ abstract class BaseFragment : Fragment(), ToolbarConfigurable {
val snowbirdGroupViewModel: SnowbirdGroupViewModel by androidViewModel()
val snowbirdRepoViewModel: SnowbirdRepoViewModel by androidViewModel()
+ val isJetpackNavigation: Boolean
+ get() {
+ val parentFragmentManager = parentFragmentManager
+ return parentFragmentManager.findFragmentById(R.id.space_nav_host_fragment) != null
+ }
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ensureComposeDialogHost()
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/core/dialog/BaseDialog.kt b/app/src/main/java/net/opendasharchive/openarchive/features/core/dialog/BaseDialog.kt
index eb6e82d9..dc18b030 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/core/dialog/BaseDialog.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/core/dialog/BaseDialog.kt
@@ -1,8 +1,8 @@
package net.opendasharchive.openarchive.features.core.dialog
-import androidx.compose.foundation.background
+import android.content.res.Configuration
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
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
@@ -16,6 +16,7 @@ import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ErrorOutline
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -26,7 +27,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@@ -54,10 +54,10 @@ fun BaseDialog(
hasCheckbox: Boolean = false,
onCheckBoxStateChanged: (Boolean) -> Unit = {},
checkBoxHint: String = "Do not show me this again",
- positiveButton: ButtonData,
+ positiveButton: ButtonData? = null,
neutralButton: ButtonData? = null,
destructiveButton: ButtonData? = null,
- backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh
+ backgroundColor: Color = MaterialTheme.colorScheme.surface
) {
val (isCheckedState, setCheckedState) = remember { mutableStateOf(false) }
@@ -70,14 +70,18 @@ fun BaseDialog(
usePlatformDefaultWidth = true
)
) {
- Box(
- Modifier
- .clip(RoundedCornerShape(12.dp))
- .fillMaxWidth()
- .background(backgroundColor)
- ) {
-
- Card {
+
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 4.dp
+ ),
+ colors = CardDefaults.cardColors(
+ containerColor = backgroundColor
+ )
+ ) {
Column(
modifier = Modifier
.fillMaxWidth()
@@ -89,11 +93,11 @@ fun BaseDialog(
icon?.let { icon ->
icon.asIcon(
contentDescription = null,
- modifier = Modifier.size(24.dp),
+ modifier = Modifier.size(30.dp),
tint = iconColor ?: Color.Unspecified
).invoke()
- Spacer(modifier = Modifier.height(18.dp))
+ Spacer(modifier = Modifier.height(8.dp))
}
BaseDialogTitle(title)
@@ -108,7 +112,7 @@ fun BaseDialog(
}
if (hasCheckbox) {
- Spacer(Modifier.height(12.dp))
+ Spacer(Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
@@ -128,19 +132,11 @@ fun BaseDialog(
}
- Spacer(Modifier.height(18.dp))
+ Spacer(Modifier.height(24.dp))
- BaseButton(
- modifier = Modifier.fillMaxWidth(),
- text = positiveButton.text.asString(),
- onClick = {
- positiveButton.action()
- onDismiss()
- })
-
- neutralButton?.let { btn ->
- Spacer(modifier = Modifier.height(4.dp))
- BaseNeutralButton(
+ positiveButton?.let { btn ->
+ Spacer(Modifier.height(4.dp))
+ BaseButton(
modifier = Modifier.fillMaxWidth(),
text = btn.text.asString(),
onClick = {
@@ -160,12 +156,23 @@ fun BaseDialog(
text = btn.text.asString()
)
}
+
+ neutralButton?.let { btn ->
+ Spacer(modifier = Modifier.height(4.dp))
+ BaseNeutralButton(
+ modifier = Modifier.fillMaxWidth(),
+ text = btn.text.asString(),
+ onClick = {
+ btn.action()
+ onDismiss()
+ })
+ }
}
}
- }
+
}
}
@@ -192,10 +199,10 @@ fun BaseDialogMessage(
Text(
text = text,
textAlign = TextAlign.Center,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium.copy(
fontSize = 14.sp,
- fontWeight = FontWeight.Normal,
+ fontWeight = FontWeight.Medium,
),
modifier = modifier
)
@@ -245,6 +252,7 @@ fun DialogHost(dialogStateManager: DialogStateManager) {
}
@Preview
+@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun BaseDialogPreview() {
DefaultBoxPreview {
@@ -262,6 +270,7 @@ private fun BaseDialogPreview() {
}
@Preview
+@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun WarningDialogPreview() {
DefaultBoxPreview {
@@ -282,6 +291,7 @@ private fun WarningDialogPreview() {
}
@Preview
+@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun ErrorDialogPreview() {
DefaultBoxPreview {
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/core/dialog/DialogConfigBuilder.kt b/app/src/main/java/net/opendasharchive/openarchive/features/core/dialog/DialogConfigBuilder.kt
index 1b48397f..91455fb9 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/core/dialog/DialogConfigBuilder.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/core/dialog/DialogConfigBuilder.kt
@@ -9,16 +9,19 @@ import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material.icons.outlined.Error
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import net.opendasharchive.openarchive.R
import net.opendasharchive.openarchive.features.core.UiImage
import net.opendasharchive.openarchive.features.core.UiText
+import net.opendasharchive.openarchive.features.core.asUiText
// --------------------------------------------------------------------
// 1. Dialog Types
@@ -34,16 +37,17 @@ data class DialogConfig(
val type: DialogType,
val title: UiText,
val message: UiText,
- val icon: UiImage?,
- val iconColor: Color?,
- val positiveButton: ButtonData,
+ val icon: UiImage? = null,
+ val iconColor: Color? = null,
+ val positiveButton: ButtonData? = null,
val neutralButton: ButtonData? = null,
val destructiveButton: ButtonData? = null,
val showCheckbox: Boolean = false,
val checkboxText: UiText? = null,
val onCheckboxChanged: (Boolean) -> Unit = {},
- val backgroundColor: Color,
- val cornerRadius: Dp
+ val backgroundColor: Color? = null,
+ val cornerRadius: Dp? = null,
+ val onDismissAction: (() -> Unit)? = null,
)
// --------------------------------------------------------------------
@@ -51,7 +55,7 @@ data class DialogConfig(
// --------------------------------------------------------------------
data class ButtonData(
val text: UiText,
- val action: () -> Unit = {}
+ val action: () -> Unit = {},
)
// --------------------------------------------------------------------
@@ -93,6 +97,9 @@ class DialogBuilder {
var checkboxText: UiText? = null
var onCheckboxChanged: (Boolean) -> Unit = {}
+ private var _onDismissAction: (() -> Unit)? = null
+
+ // Button DSL functions – simple and concise
fun positiveButton(block: ButtonBuilder.() -> Unit) {
_positiveButton = ButtonBuilder().apply(block)
.build(defaultText = defaultPositiveTextFor(type))
@@ -126,18 +133,13 @@ class DialogBuilder {
fun build(): DialogConfig {
if (icon == null) {
-
- val defaultIconVector = when (type) {
- DialogType.Success -> Icons.Filled.Check
- DialogType.Error -> Icons.Default.Error
- DialogType.Warning -> Icons.Default.Warning
- DialogType.Info -> Icons.Filled.Info
+ icon = when (type) {
+ DialogType.Success -> UiImage.DrawableResource(R.drawable.ic_done)
+ DialogType.Error -> UiImage.DynamicVector(Icons.Outlined.Error)
+ DialogType.Warning -> UiImage.DynamicVector(Icons.Default.Warning)
+ DialogType.Info -> UiImage.DynamicVector(Icons.Filled.Info)
DialogType.Custom -> null
}
-
- if (defaultIconVector != null) {
- icon = UiImage.DynamicVector(defaultIconVector)
- }
}
val finalIconColor = iconColor ?: when (type) {
@@ -160,14 +162,15 @@ class DialogBuilder {
message = message ?: UiText.DynamicString(""),
icon = icon,
iconColor = finalIconColor,
- positiveButton = _positiveButton ?: ButtonData(defaultPositiveTextFor(type)),
+ positiveButton = _positiveButton, //?: ButtonData(defaultPositiveTextFor(type)),
neutralButton = _neutralButton,
destructiveButton = _destructiveButton,
showCheckbox = showCheckbox,
checkboxText = checkboxText,
onCheckboxChanged = onCheckboxChanged,
backgroundColor = finalBackgroundColor,
- cornerRadius = finalCornerRadius
+ cornerRadius = finalCornerRadius,
+ onDismissAction = _onDismissAction
)
}
@@ -178,17 +181,13 @@ class DialogBuilder {
if (icon == null) {
- val defaultIconVector = when (type) {
- DialogType.Success -> Icons.Filled.Check
- DialogType.Error -> Icons.Default.Error
- DialogType.Warning -> Icons.Default.Warning
- DialogType.Info -> Icons.Filled.Info
+ icon = when (type) {
+ DialogType.Success -> UiImage.DrawableResource(R.drawable.ic_done)
+ DialogType.Error -> UiImage.DynamicVector(Icons.Outlined.Error)
+ DialogType.Warning -> UiImage.DynamicVector(Icons.Default.Warning)
+ DialogType.Info -> UiImage.DynamicVector(Icons.Filled.Info)
DialogType.Custom -> null
}
-
- if (defaultIconVector != null) {
- icon = UiImage.DynamicVector(defaultIconVector)
- }
}
// Convert resource colors (ints) to Compose Colors.
@@ -212,7 +211,7 @@ class DialogBuilder {
message = message ?: UiText.DynamicString(""),
icon = icon,
iconColor = finalIconColor,
- positiveButton = _positiveButton ?: ButtonData(defaultPositiveTextFor(type)),
+ positiveButton = _positiveButton, //?: ButtonData(defaultPositiveTextFor(type)),
neutralButton = _neutralButton,
destructiveButton = _destructiveButton,
showCheckbox = showCheckbox,
@@ -236,7 +235,7 @@ fun DialogStateManager.showDialog(block: DialogBuilder.() -> Unit) {
}
// --- View extension: pass a Context so that resource colors are used.
-fun DialogStateManager.showDialog(resourceProvider: ResourceProvider, block: DialogBuilder.() -> Unit) {
+fun DialogStateManager.showDialog(resourceProvider: ResourceProvider = this.requireResourceProvider(), block: DialogBuilder.() -> Unit) {
val config = DialogBuilder().apply(block).build(resourceProvider)
showDialog(config)
}
@@ -275,7 +274,7 @@ fun DialogStateManager.showSuccessDialog(
val resourceProvider = this.requireResourceProvider()
showDialog(resourceProvider) {
- type = DialogType.Info
+ type = DialogType.Success
if (icon != null) this.icon = icon
if (title != null) this.title = UiText.StringResource(title)
this.message = UiText.StringResource(message)
@@ -332,6 +331,60 @@ fun DialogStateManager.showInfoDialog(
}
}
+// View helper for an info/hint dialog.
+fun DialogStateManager.showWarningDialog(
+ title: UiText?,
+ message: UiText,
+ icon: UiImage? = null,
+ positiveButtonText: UiText? = null,
+ onDone: () -> Unit = {},
+ onCancel: () -> Unit = {}
+) {
+ val resourceProvider = this.requireResourceProvider()
+
+ showDialog(resourceProvider) {
+ type = DialogType.Warning
+ this.title = title
+ this.icon = icon
+ this.message = message
+ positiveButton {
+ text = positiveButtonText ?: UiText.StringResource(R.string.lbl_got_it)
+ action = onDone
+ }
+ destructiveButton {
+ text = UiText.StringResource(R.string.lbl_Cancel)
+ action = onCancel
+ }
+ }
+}
+
+// For Destructive Actions confirmation (Removing folder or server etc)
+fun DialogStateManager.showDestructiveDialog(
+ title: UiText?,
+ message: UiText,
+ icon: UiImage? = null,
+ positiveButtonText: UiText? = null,
+ onDone: () -> Unit = {},
+ onCancel: () -> Unit = {}
+) {
+ val resourceProvider = this.requireResourceProvider()
+
+ showDialog(resourceProvider) {
+ type = DialogType.Warning
+ this.title = title
+ this.icon = icon
+ this.message = message
+ positiveButton {
+ text = positiveButtonText ?: UiText.StringResource(R.string.lbl_got_it)
+ action = onDone
+ }
+ destructiveButton {
+ text = UiText.StringResource(R.string.lbl_Cancel)
+ action = onCancel
+ }
+ }
+}
+
/**
* ResourceProvider is an abstraction that lets you look up colors and vector icons
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderActivity.kt
index 12dd7285..f04c5ee0 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderActivity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderActivity.kt
@@ -3,14 +3,13 @@ package net.opendasharchive.openarchive.features.folders
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
+import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
-import net.opendasharchive.openarchive.R
-import net.opendasharchive.openarchive.databinding.ActivityAddFolderBinding
+import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme
import net.opendasharchive.openarchive.db.Space
import net.opendasharchive.openarchive.features.core.BaseActivity
import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity
-import net.opendasharchive.openarchive.util.extensions.hide
class AddFolderActivity : BaseActivity() {
@@ -19,7 +18,6 @@ class AddFolderActivity : BaseActivity() {
const val EXTRA_FOLDER_NAME = "folder_name"
}
- private lateinit var mBinding: ActivityAddFolderBinding
private lateinit var mResultLauncher: ActivityResultLauncher
override fun onCreate(savedInstanceState: Bundle?) {
@@ -34,7 +32,7 @@ class AddFolderActivity : BaseActivity() {
val name = it.data?.getStringExtra(EXTRA_FOLDER_NAME)
if (!name.isNullOrBlank()) {
- val i = Intent(this, CreateNewFolderActivity::class.java)
+ val i = Intent(this, CreateNewFolderFragment::class.java)
i.putExtra(EXTRA_FOLDER_NAME, name)
mResultLauncher.launch(i)
@@ -42,27 +40,35 @@ class AddFolderActivity : BaseActivity() {
}
}
- mBinding = ActivityAddFolderBinding.inflate(layoutInflater)
- setContentView(mBinding.root)
+ //mBinding = ActivityAddFolderBinding.inflate(layoutInflater)
+ //setContentView(mBinding.root)
- setupToolbar(
- title = getString(R.string.add_a_folder),
- showBackButton = true
- )
- mBinding.addFolderContainer.setOnClickListener {
- setFolder(false)
- }
+ setContent {
+
+ SaveAppTheme {
- mBinding.browseFolderContainer.setOnClickListener {
- setFolder(true)
+ AddFolderScreen(
+// onCreateFolder = {
+// setFolder(browse = false)
+// },
+// onBrowseFolders = {
+// setFolder(browse = true)
+// },
+// onNavigateBack = {
+// finish()
+// }
+ )
+ }
}
+
+
// We cannot browse the Internet Archive. Directly forward to creating a project,
// as it doesn't make sense to show a one-option menu.
if (Space.current?.tType == Space.Type.INTERNET_ARCHIVE) {
- mBinding.browseFolderContainer.hide()
+ //mBinding.browseFolderContainer.hide()
finish()
setFolder(false)
@@ -92,7 +98,7 @@ class AddFolderActivity : BaseActivity() {
mResultLauncher.launch(
Intent(
this,
- if (browse) BrowseFoldersActivity::class.java else CreateNewFolderActivity::class.java
+ if (browse) BrowseFoldersFragment::class.java else CreateNewFolderFragment::class.java
)
)
}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderScreen.kt
new file mode 100644
index 00000000..6444a253
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderScreen.kt
@@ -0,0 +1,164 @@
+package net.opendasharchive.openarchive.features.folders
+
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
+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.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.filled.ArrowForward
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.findNavController
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview
+import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme
+
+@Composable
+fun AddFolderScreen() {
+
+ val navController = LocalView.current.findNavController()
+
+ SaveAppTheme {
+ AddFolderScreenContent(
+ onCreateFolder = {
+ navController.navigate(R.id.fragment_add_folder_to_fragment_create_new_folder)
+ },
+ onBrowseFolders = {
+ navController.navigate(R.id.fragment_add_folder_to_fragment_browse_folders)
+ }
+ )
+ }
+
+}
+
+
+@Composable
+fun AddFolderScreenContent(
+ onCreateFolder: () -> Unit,
+ onBrowseFolders: () -> Unit
+) {
+
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding()
+ .padding(vertical = 24.dp)
+ .verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.height(32.dp))
+
+ Text(
+ text = stringResource(id = R.string.select_where_to_store_your_media),
+ fontSize = 18.sp,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.SemiBold,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(horizontal = 64.dp)
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ FolderOption(
+ iconRes = R.drawable.ic_create_new_folder,
+ text = stringResource(id = R.string.create_a_new_folder),
+ onClick = onCreateFolder
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ FolderOption(
+ iconRes = R.drawable.ic_browse_existing_folders,
+ text = stringResource(id = R.string.browse_existing_folders),
+ onClick = onBrowseFolders
+ )
+ }
+}
+
+
+@Composable
+fun FolderOption(iconRes: Int, text: String, onClick: () -> Unit) {
+
+ Card(
+ modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
+ onClick = onClick,
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.background
+ ),
+ shape = RoundedCornerShape(8.dp),
+ border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.onBackground)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp, vertical = 24.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(id = iconRes),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.tertiary
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ Text(
+ text = text,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.weight(1f)
+ )
+
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowForward,
+ contentDescription = null,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+}
+
+
+@Preview
+@Preview(uiMode = UI_MODE_NIGHT_YES)
+@Composable
+private fun AddFolderScreenPreview() {
+ DefaultScaffoldPreview {
+ AddFolderScreenContent(
+ onCreateFolder = {},
+ onBrowseFolders = {}
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFolderScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFolderScreen.kt
new file mode 100644
index 00000000..67dd1470
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFolderScreen.kt
@@ -0,0 +1,103 @@
+package net.opendasharchive.openarchive.features.folders
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.findNavController
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview
+import org.koin.androidx.compose.koinViewModel
+import java.util.Date
+
+@Composable
+fun BrowseFolderScreen(
+ viewModel: BrowseFoldersViewModel = koinViewModel()
+) {
+
+ val navController = LocalView.current.findNavController()
+
+
+ val folders by viewModel.folders.observeAsState()
+
+
+ BrowseFolderScreenContent(
+ folders = folders ?: emptyList()
+ )
+}
+
+
+@Composable
+fun BrowseFolderScreenContent(
+ folders: List
+) {
+
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(vertical = 24.dp, horizontal = 16.dp),
+ contentPadding = PaddingValues(16.dp)
+ ) {
+
+ items(folders) { folder ->
+ BrowseFolderItem(folder) { }
+ }
+ }
+
+}
+
+@Composable
+fun BrowseFolderItem(
+ folder: Folder,
+ onClick: () -> Unit
+) {
+
+ Card(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+
+ Icon(painter = painterResource(R.drawable.ic_folder_new), contentDescription = null)
+ Text(folder.name)
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun BrowseFolderScreenPreview() {
+ DefaultScaffoldPreview {
+ BrowseFolderScreenContent(
+ folders = listOf(
+ Folder(name = "Elelan", modified = Date()),
+ Folder(name = "Save", modified = Date()),
+ Folder(name = "Downloads", modified = Date()),
+ Folder(name = "Trip", modified = Date()),
+ Folder(name = "Wedding", modified = Date()),
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersActivity.kt
deleted file mode 100644
index a04be3d3..00000000
--- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersActivity.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-package net.opendasharchive.openarchive.features.folders
-
-import android.content.Intent
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import androidx.activity.viewModels
-import androidx.recyclerview.widget.LinearLayoutManager
-import net.opendasharchive.openarchive.R
-import net.opendasharchive.openarchive.databinding.ActivityBrowseFoldersBinding
-import net.opendasharchive.openarchive.db.Project
-import net.opendasharchive.openarchive.db.Space
-import net.opendasharchive.openarchive.features.core.BaseActivity
-import net.opendasharchive.openarchive.util.extensions.toggle
-import java.util.Date
-
-
-class BrowseFoldersActivity : BaseActivity() {
-
- private lateinit var mBinding: ActivityBrowseFoldersBinding
- private val mViewModel: BrowseFoldersViewModel by viewModels()
-
- private var mSelected: Folder? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- mBinding = ActivityBrowseFoldersBinding.inflate(layoutInflater)
- setContentView(mBinding.root)
-
- setupToolbar(
- title = getString(R.string.browse_existing),
- showBackButton = true
- )
-
- mBinding.rvFolderList.layoutManager = LinearLayoutManager(this)
-
- val space = Space.current
- if (space != null) mViewModel.getFiles(this, space)
-
- mViewModel.folders.observe(this) {
- mBinding.projectsEmpty.toggle(it.isEmpty())
-
- mBinding.rvFolderList.adapter = BrowseFoldersAdapter(it) { folder ->
- this.mSelected = folder
- invalidateOptionsMenu()
- }
- }
-
- mViewModel.progressBarFlag.observe(this) {
- mBinding.progressBar.toggle(it)
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_browse_folder, menu)
-
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
- val addMenuItem = menu?.findItem(R.id.action_add)
- addMenuItem?.isVisible = mSelected != null
- return super.onPrepareOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.action_add -> {
- addFolder(mSelected)
- return true
- }
- }
-
- return super.onOptionsItemSelected(item)
- }
-
- private fun addFolder(folder: Folder?) {
- if (folder == null) return
- val space = Space.current ?: return
-
- // This should not happen. These should have been filtered on display.
- if (space.hasProject(folder.name)) return
-
- val license = space.license
-
-// if (license.isNullOrBlank()) {
-// val i = Intent()
-// i.putExtra(AddFolderActivity.EXTRA_FOLDER_NAME, folder.name)
-//
-// setResult(RESULT_CANCELED, i)
-// }
-// else {
- val project = Project(folder.name, Date(), space.id, licenseUrl = license)
- project.save()
-
- val i = Intent()
- i.putExtra(AddFolderActivity.EXTRA_FOLDER_ID, project.id)
-
- setResult(RESULT_OK, i)
-// }
-
- finish()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt
index cc86d19b..1fd1ec01 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt
@@ -22,7 +22,6 @@ class BrowseFoldersAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
val binding = FolderRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- val context = binding.root.context
return FolderViewHolder(binding, onClick)
}
@@ -42,10 +41,9 @@ class BrowseFoldersAdapter(
itemView.isSelected = isSelected
- val folderIconRes = if (isSelected) R.drawable.ic_folder_selected else R.drawable.ic_folder_unselected
-
- binding.icon.setImageDrawable(ContextCompat.getDrawable(binding.icon.context, folderIconRes))
-
+ val icon = ContextCompat.getDrawable(binding.icon.context, R.drawable.ic_folder_new)
+ icon?.setTint(ContextCompat.getColor(binding.icon.context, R.color.colorOnBackground))
+ binding.icon.setImageDrawable(icon)
binding.name.text = folder.name
binding.timestamp.text = formatter.format(folder.modified)
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersFragment.kt
new file mode 100644
index 00000000..042c8a81
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersFragment.kt
@@ -0,0 +1,126 @@
+package net.opendasharchive.openarchive.features.folders
+
+import android.app.Activity
+import android.app.Activity.RESULT_OK
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.MenuProvider
+import androidx.lifecycle.Lifecycle
+import androidx.recyclerview.widget.LinearLayoutManager
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.databinding.FragmentBrowseFoldersBinding
+import net.opendasharchive.openarchive.db.Project
+import net.opendasharchive.openarchive.db.Space
+import net.opendasharchive.openarchive.features.core.BaseFragment
+import net.opendasharchive.openarchive.features.core.dialog.showSuccessDialog
+import net.opendasharchive.openarchive.util.extensions.toggle
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import java.util.Date
+
+
+class BrowseFoldersFragment : BaseFragment(), MenuProvider {
+
+ private lateinit var mBinding: FragmentBrowseFoldersBinding
+ private val mViewModel: BrowseFoldersViewModel by viewModel()
+
+ private var mSelected: Folder? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ mBinding = FragmentBrowseFoldersBinding.inflate(layoutInflater)
+
+ mBinding.rvFolderList.layoutManager = LinearLayoutManager(requireContext())
+
+ return mBinding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
+
+ val space = Space.current
+ if (space != null) mViewModel.getFiles(space)
+
+ mViewModel.folders.observe(viewLifecycleOwner) {
+ mBinding.projectsEmpty.toggle(it.isEmpty())
+
+ mBinding.rvFolderList.adapter = BrowseFoldersAdapter(it) { folder ->
+ this.mSelected = folder
+ activity?.invalidateOptionsMenu()
+ }
+ }
+
+ mViewModel.progressBarFlag.observe(viewLifecycleOwner) {
+ mBinding.progressBar.toggle(it)
+ }
+ }
+
+
+ override fun getToolbarTitle(): String = getString(R.string.browse_existing)
+
+ private fun addFolder(folder: Folder?) {
+ if (folder == null) return
+ val space = Space.current ?: return
+
+ // This should not happen. These should have been filtered on display.
+ if (space.hasProject(folder.name)) return
+
+ val license = space.license
+
+
+ val project = Project(folder.name, Date(), space.id, licenseUrl = license)
+ project.save()
+
+ showFolderCreated(project.id)
+ }
+
+ private fun showFolderCreated(projectId: Long) {
+
+ dialogManager.showSuccessDialog(
+ title = R.string.label_success_title,
+ message = R.string.create_folder_ok_message,
+ positiveButtonText = R.string.label_got_it,
+ onDone = {
+ navigateBackWithResult(projectId)
+ }
+ )
+ }
+
+ private fun navigateBackWithResult(projectId: Long) {
+ requireActivity().setResult(RESULT_OK, Intent().apply {
+ putExtra(AddFolderActivity.EXTRA_FOLDER_ID, projectId)
+ })
+ requireActivity().finish()
+ }
+
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.menu_browse_folder, menu)
+ }
+
+ override fun onPrepareMenu(menu: Menu) {
+ super.onPrepareMenu(menu)
+ val addMenuItem = menu.findItem(R.id.action_add)
+ addMenuItem?.isVisible = mSelected != null
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ return when (menuItem.itemId) {
+ R.id.action_add -> {
+ addFolder(mSelected)
+ true
+ }
+
+ else -> false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt
index 3eb5945b..0f1055df 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt
@@ -15,9 +15,11 @@ import timber.log.Timber
import java.io.IOException
import java.util.Date
+
+
data class Folder(val name: String, val modified: Date)
-class BrowseFoldersViewModel : ViewModel() {
+class BrowseFoldersViewModel(private val context: Context) : ViewModel() {
private val mFolders = MutableLiveData>()
@@ -26,7 +28,7 @@ class BrowseFoldersViewModel : ViewModel() {
val progressBarFlag = MutableLiveData(false)
- fun getFiles(context: Context, space: Space) {
+ fun getFiles(space: Space) {
viewModelScope.launch {
progressBarFlag.value = true
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderActivity.kt
deleted file mode 100644
index de122cc6..00000000
--- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderActivity.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-package net.opendasharchive.openarchive.features.folders
-
-import android.content.Intent
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import android.view.inputmethod.EditorInfo
-import android.widget.Toast
-import net.opendasharchive.openarchive.R
-import net.opendasharchive.openarchive.databinding.ActivityCreateNewFolderBinding
-import net.opendasharchive.openarchive.db.Project
-import net.opendasharchive.openarchive.db.Space
-import net.opendasharchive.openarchive.features.core.BaseActivity
-import net.opendasharchive.openarchive.features.core.dialog.showSuccessDialog
-import net.opendasharchive.openarchive.features.settings.CcSelector
-import net.opendasharchive.openarchive.util.extensions.hide
-import java.util.Date
-
-class CreateNewFolderActivity : BaseActivity() {
-
- companion object {
- private const val SPECIAL_CHARS = ".*[\\\\/*\\s]"
- }
-
- private lateinit var mBinding: ActivityCreateNewFolderBinding
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- mBinding = ActivityCreateNewFolderBinding.inflate(layoutInflater)
- setContentView(mBinding.root)
-
- setupToolbar(
- title = "Create Folder",
- showBackButton = true
- )
-
- mBinding.newFolder.setText(intent.getStringExtra(AddFolderActivity.EXTRA_FOLDER_NAME))
-
- mBinding.newFolder.setOnEditorActionListener { _, actionId, _ ->
- if (actionId == EditorInfo.IME_ACTION_DONE) {
- store()
- }
-
- false
- }
-
- if (Space.current?.license != null) {
- mBinding.cc.root.hide()
- }
- else {
- CcSelector.init(mBinding.cc)
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_new_folder, menu)
-
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- android.R.id.home -> {
- finish()
- return true
- }
- R.id.action_done -> {
- store()
- return true
- }
- }
- return super.onOptionsItemSelected(item)
- }
-
- private fun store() {
- val name = mBinding.newFolder.text.toString()
-
- if (name.isBlank()) return
-
- if (name.matches(SPECIAL_CHARS.toRegex())) {
- Toast.makeText(this,
- getString(R.string.please_do_not_include_special_characters_in_the_name),
- Toast.LENGTH_SHORT).show()
-
- return
- }
-
- val space = Space.current ?: return
-
- if (space.hasProject(name)) {
- Toast.makeText(this, getString(R.string.folder_name_already_exists),
- Toast.LENGTH_LONG).show()
-
- return
- }
-
- val license = space.license ?: CcSelector.get(mBinding.cc)
-
- val project = Project(name, Date(), space.id, licenseUrl = license)
- project.save()
-
- showFolderCreated(project.id)
-
-
- }
-
- private fun showFolderCreated(projectId: Long) {
-
- dialogManager.showSuccessDialog(
- title = R.string.label_success_title,
- message = R.string.create_folder_ok_message,
- positiveButtonText = R.string.label_got_it,
- onDone = {
- navigateBackWithResult(projectId)
- }
- )
- }
-
- private fun navigateBackWithResult(projectId: Long) {
- val i = Intent()
- i.putExtra(AddFolderActivity.EXTRA_FOLDER_ID, projectId)
-
- setResult(RESULT_OK, i)
- finish()
- }
-}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderFragment.kt
new file mode 100644
index 00000000..7386f33b
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderFragment.kt
@@ -0,0 +1,150 @@
+package net.opendasharchive.openarchive.features.folders
+
+import android.app.Activity.RESULT_CANCELED
+import android.app.Activity.RESULT_OK
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import android.widget.Toast
+import androidx.core.view.MenuProvider
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.databinding.FragmentCreateNewFolderBinding
+import net.opendasharchive.openarchive.db.Project
+import net.opendasharchive.openarchive.db.Space
+import net.opendasharchive.openarchive.features.core.BaseFragment
+import net.opendasharchive.openarchive.features.core.dialog.showSuccessDialog
+import net.opendasharchive.openarchive.features.settings.CreativeCommonsLicenseManager
+import net.opendasharchive.openarchive.util.extensions.hide
+import java.util.Date
+
+class CreateNewFolderFragment : BaseFragment() {
+
+ companion object {
+ private const val SPECIAL_CHARS = ".*[\\\\/*\\s]"
+ }
+
+ private lateinit var binding: FragmentCreateNewFolderBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentCreateNewFolderBinding.inflate(layoutInflater)
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val intent = requireActivity().intent
+
+ binding.newFolder.setText(intent.getStringExtra(AddFolderActivity.EXTRA_FOLDER_NAME))
+
+ binding.newFolder.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ store()
+ }
+
+ false
+ }
+
+ binding.btnSubmit.setOnClickListener {
+ store()
+ }
+
+ binding.btnCancel.setOnClickListener {
+ requireActivity().setResult(RESULT_CANCELED)
+ requireActivity().finish()
+ }
+
+ setupTextWatchers()
+ }
+
+ private fun setupTextWatchers() {
+ // Create a common TextWatcher for all three fields
+ val textWatcher = object : android.text.TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ updateAuthenticateButtonState()
+ }
+
+ override fun afterTextChanged(s: android.text.Editable?) {}
+ }
+
+ binding.newFolder.addTextChangedListener(textWatcher)
+ }
+
+ private fun updateAuthenticateButtonState() {
+ val folderName = binding.newFolder.text?.toString()?.trim().orEmpty()
+
+ // Enable the button only if none of the fields are empty
+ binding.btnSubmit.isEnabled = folderName.isNotEmpty()
+ }
+
+ override fun getToolbarTitle(): String = getString(R.string.create_a_new_folder)
+
+ private fun store() {
+ val name = binding.newFolder.text.toString()
+
+ if (name.isBlank()) return
+
+ if (name.matches(SPECIAL_CHARS.toRegex())) {
+ Toast.makeText(
+ requireContext(),
+ getString(R.string.please_do_not_include_special_characters_in_the_name),
+ Toast.LENGTH_SHORT
+ ).show()
+
+ return
+ }
+
+ val space = Space.current ?: return
+
+ if (space.hasProject(name)) {
+ Toast.makeText(
+ requireContext(), getString(R.string.folder_name_already_exists),
+ Toast.LENGTH_LONG
+ ).show()
+
+ return
+ }
+
+ val license =
+ space.license ?: CreativeCommonsLicenseManager.getSelectedLicenseUrl(binding.cc)
+
+ val project = Project(name, Date(), space.id, licenseUrl = license)
+ project.save()
+
+ showFolderCreated(project.id)
+
+
+ }
+
+ private fun showFolderCreated(projectId: Long) {
+
+ dialogManager.showSuccessDialog(
+ title = R.string.label_success_title,
+ message = R.string.create_folder_ok_message,
+ positiveButtonText = R.string.label_got_it,
+ onDone = {
+ navigateBackWithResult(projectId)
+ }
+ )
+ }
+
+ private fun navigateBackWithResult(projectId: Long) {
+ val i = Intent()
+ i.putExtra(AddFolderActivity.EXTRA_FOLDER_ID, projectId)
+
+ requireActivity().setResult(RESULT_OK, i)
+ requireActivity().finish()
+ }
+}
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 05b47be5..b5cf6fda 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
@@ -11,13 +11,19 @@ import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier
import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme
import net.opendasharchive.openarchive.db.Space
+import net.opendasharchive.openarchive.features.core.dialog.DialogHost
+import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager
import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult
import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace
import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ComposeAppBar
import net.opendasharchive.openarchive.features.main.MainActivity
+import org.koin.androidx.viewmodel.ext.android.viewModel
@Deprecated("use jetpack compose")
class InternetArchiveActivity : AppCompatActivity() {
+
+ val dialogManager: DialogStateManager by viewModel()
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -26,11 +32,14 @@ class InternetArchiveActivity : AppCompatActivity() {
setContent {
SaveAppTheme {
+
+ DialogHost(dialogManager)
+
Scaffold(
topBar = {
ComposeAppBar(
- title = if (isNewSpace) "Add Internet Archive" else "Edit Internet Archive",
- onNavigationAction = { finish(IAResult.Cancelled) }
+ title = if (isNewSpace) "Internet Archive" else "Internet Archive",
+ onNavigationAction = { onComplete(IAResult.Cancelled) }
)
}
) { paddingValues ->
@@ -38,7 +47,7 @@ class InternetArchiveActivity : AppCompatActivity() {
.fillMaxSize()
.padding(paddingValues)) {
InternetArchiveScreen(space, isNewSpace) {
- finish(it)
+ onComplete(it)
}
}
}
@@ -48,7 +57,7 @@ class InternetArchiveActivity : AppCompatActivity() {
}
}
- private fun finish(result: IAResult) {
+ private fun onComplete(result: IAResult) {
when (result) {
IAResult.Saved -> {
startActivity(Intent(this, MainActivity::class.java))
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 b88e954e..950267db 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
@@ -7,6 +7,8 @@ import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
+import androidx.navigation.fragment.findNavController
+import net.opendasharchive.openarchive.R
import net.opendasharchive.openarchive.db.Space
import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult
import net.opendasharchive.openarchive.features.internetarchive.presentation.components.bundleWithNewSpace
@@ -36,10 +38,22 @@ class InternetArchiveFragment : BaseFragment(), ToolbarConfigurable {
}
private fun finish(result: IAResult) {
- setFragmentResult(result.value, bundleOf())
+ if (isJetpackNavigation) {
+ when (result) {
+ IAResult.Saved -> {
+ val message = getString(R.string.you_have_successfully_connected_to_the_internet_archive)
+ val action = InternetArchiveFragmentDirections.actionFragmentInternetArchiveToFragmentSpaceSetupSuccess(message)
+ findNavController().navigate(action)
+ }
+ IAResult.Deleted -> TODO()
+ IAResult.Cancelled -> findNavController().popBackStack()
+ }
+ } else {
+ setFragmentResult(result.value, bundleOf())
- if (result == IAResult.Saved) {
- // activity?.measureNewBackend(Space.Type.INTERNET_ARCHIVE)
+ if (result == IAResult.Saved) {
+ // activity?.measureNewBackend(Space.Type.INTERNET_ARCHIVE)
+ }
}
}
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
index 177d5615..8a955ad7 100644
--- 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
@@ -8,7 +8,7 @@ import net.opendasharchive.openarchive.db.Space
private const val ARG_VAL_NEW_SPACE = -1L
@Deprecated("only for use with fragments and activities")
-private const val ARG_SPACE = "space"
+private const val ARG_SPACE = "space_id"
@Deprecated("only for use with fragments and activities")
enum class IAResult(
@@ -25,7 +25,7 @@ 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 mSpaceId = this?.getLong(ARG_SPACE) ?: ARG_VAL_NEW_SPACE
val isNewSpace = ARG_VAL_NEW_SPACE == mSpaceId
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 bd53c808..3885aec2 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt
@@ -18,6 +18,7 @@ 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
@@ -28,7 +29,7 @@ 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) {
+fun InternetArchiveHeader(modifier: Modifier = Modifier) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
@@ -36,28 +37,28 @@ fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 1
) {
Box(
modifier = Modifier
- .size(ThemeDimensions.touchable)
- .background(
- color = ThemeColors.material.surface,
- shape = CircleShape
- )
+ .size(50.dp)
.clip(CircleShape)
+ .background(ThemeColors.material.surfaceDim,)
+ .padding(8.dp),
+ contentAlignment = Alignment.Center
) {
Image(
- modifier = Modifier
- .matchParentSize()
- .padding(11.dp),
+ modifier = Modifier.size(30.dp),
painter = painterResource(id = R.drawable.ic_internet_archive),
- contentDescription = stringResource(
- id = R.string.internet_archive
- ),
+ contentDescription = stringResource(R.string.internet_archive),
colorFilter = tint(colorResource(id = R.color.colorTertiary))
)
}
- Column(modifier = Modifier.padding(start = ThemeDimensions.spacing.medium)) {
+
+ Column(
+ modifier = Modifier.padding(start = ThemeDimensions.spacing.medium, end = ThemeDimensions.spacing.xlarge)
+ ) {
Text(
text = stringResource(id = R.string.internet_archive_description),
- color = ThemeColors.material.onSurfaceVariant
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Medium,
+ 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
index b05abbbc..b006feb4 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
@@ -1,16 +1,15 @@
package net.opendasharchive.openarchive.features.internetarchive.presentation.details
+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.shape.RoundedCornerShape
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -18,26 +17,23 @@ 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.graphics.Color
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.presentation.theme.SaveAppTheme
-import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors
+import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview
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.core.UiImage
+import net.opendasharchive.openarchive.features.core.UiText
+import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager
+import net.opendasharchive.openarchive.features.core.dialog.showDialog
import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult
-import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader
import net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsViewModel.Action
import net.opendasharchive.openarchive.features.internetarchive.presentation.login.CustomTextField
-import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
@@ -65,12 +61,10 @@ fun InternetArchiveDetailsScreen(space: Space, onResult: (IAResult) -> Unit) {
@Composable
private fun InternetArchiveDetailsContent(
state: InternetArchiveDetailsState,
- dispatch: Dispatch
+ dispatch: Dispatch,
+ dialogManager: DialogStateManager = koinViewModel()
) {
- var isRemoving by remember { mutableStateOf(false) }
-
-
Box(
modifier = Modifier
.fillMaxSize()
@@ -79,7 +73,7 @@ private fun InternetArchiveDetailsContent(
Column {
- InternetArchiveHeader()
+ //InternetArchiveHeader()
Spacer(Modifier.height(ThemeDimensions.spacing.large))
@@ -108,64 +102,52 @@ private fun InternetArchiveDetailsContent(
onValueChange = {},
enabled = false,
)
- }
- Button(
- modifier = Modifier
- .padding(12.dp)
- .align(Alignment.BottomCenter),
- onClick = {
- isRemoving = true
- },
- colors = ButtonDefaults.buttonColors(
- containerColor = ThemeColors.material.error,
- contentColor = Color.White
- )
- ) {
- Text(stringResource(id = R.string.menu_delete))
- }
- }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 24.dp),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ TextButton(
+ onClick = {
+ //isRemoving = true
+
+ dialogManager.showDialog(dialogManager.requireResourceProvider()) {
+ title = UiText.StringResource(R.string.remove_from_app)
+ message = UiText.StringResource(R.string.are_you_sure_you_want_to_remove_this_server_from_the_app)
+ icon = UiImage.DrawableResource(R.drawable.ic_trash)
+ destructiveButton {
+ text = UiText.StringResource(R.string.remove)
+ action = {
+ dispatch(Action.Remove)
+ }
+ }
+
+ neutralButton {
+ text = UiText.StringResource(R.string.action_cancel)
+ action = {
+ //dismiss
+ }
+ }
+ }
+ },
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text(
+ stringResource(id = R.string.remove_from_app),
+ fontSize = 18.sp
+ )
+ }
+ }
+
- 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 = {
- TextButton(
- onClick = onDismiss,
- colors = ButtonDefaults.textButtonColors(
- contentColor = MaterialTheme.colorScheme.onPrimaryContainer
- )
- ) {
- Text(stringResource(id = R.string.action_cancel))
- }
- }, confirmButton = {
- Button(
- onClick = onRemove,
- shape = RoundedCornerShape(ThemeDimensions.roundedCorner),
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.errorContainer,
- contentColor = MaterialTheme.colorScheme.onPrimaryContainer
- )
- ) {
- Text(stringResource(id = R.string.remove))
- }
- })
+
+ }
}
@Composable
@@ -178,16 +160,9 @@ private fun InternetArchiveScreenPreview() {
email = "abc@example.com",
userName = "@abc_name",
screenName = "ABC Name"
- )
- ) {}
+ ),
+ dispatch = {}
+ )
}
}
-@Composable
-@Preview(showBackground = true)
-@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
-private fun RemoveInternetArchiveDialogPreview() {
- SaveAppTheme {
- RemoveInternetArchiveDialog(onDismiss = { }) {}
- }
-}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt
index dd6b636d..d4e66d85 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt
@@ -8,6 +8,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -23,11 +24,14 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ColorScheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
@@ -43,8 +47,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -54,6 +60,7 @@ import androidx.compose.ui.text.input.PlatformImeOptions
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.delay
import net.opendasharchive.openarchive.R
@@ -111,12 +118,6 @@ private fun InternetArchiveLoginContent(
state: InternetArchiveLoginState, dispatch: Dispatch
) {
- // If extra paranoid could pre-hash password in memory
- // and use the store/dispatcher
- var showPassword by rememberSaveable {
- mutableStateOf(false)
- }
-
LaunchedEffect(state.isLoginError) {
while (state.isLoginError) {
delay(3000)
@@ -127,19 +128,38 @@ private fun InternetArchiveLoginContent(
Column(
modifier = Modifier
.fillMaxSize()
- .padding(16.dp),
+ .padding(top = 32.dp, bottom = 16.dp)
+ .padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
InternetArchiveHeader(
- modifier = Modifier.padding(bottom = ThemeDimensions.spacing.large)
+ modifier = Modifier
+ .padding(vertical = 48.dp)
+ .padding(end = 24.dp)
)
+
+
+ Box {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp)
+ ) {
+ Text(
+ "Account",
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 18.sp
+ )
+ }
+ }
+
CustomTextField(
value = state.username,
onValueChange = { dispatch(UpdateUsername(it)) },
label = stringResource(R.string.label_username),
- placeholder = stringResource(R.string.placeholder_email_or_username),
+ placeholder = stringResource(R.string.prompt_email),
isError = state.isUsernameError,
isLoading = state.isBusy,
keyboardType = KeyboardType.Email,
@@ -152,41 +172,48 @@ private fun InternetArchiveLoginContent(
value = state.password,
onValueChange = { dispatch(UpdatePassword(it)) },
label = stringResource(R.string.label_password),
- placeholder = stringResource(R.string.placeholder_password),
+ placeholder = stringResource(R.string.prompt_password),
isError = state.isPasswordError,
isLoading = state.isBusy,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
)
- Spacer(Modifier.height(ThemeDimensions.spacing.large))
-
- AnimatedVisibility(
- visible = state.isLoginError,
- enter = fadeIn(),
- exit = fadeOut()
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Start
) {
- Text(
- text = stringResource(R.string.error_incorrect_username_or_password),
- color = MaterialTheme.colorScheme.error
- )
+ AnimatedVisibility(
+ visible = state.isLoginError,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Text(
+ text = stringResource(R.string.error_incorrect_username_or_password),
+ color = MaterialTheme.colorScheme.error
+ )
+ }
}
+
+ Spacer(Modifier.height(ThemeDimensions.spacing.large))
Row(
modifier = Modifier
- .padding(top = ThemeDimensions.spacing.small)
- .weight(1f),
+ .padding(top = ThemeDimensions.spacing.small),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.prompt_no_account),
- color = ThemeColors.material.onBackground
+ color = ThemeColors.material.onBackground,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 16.sp
)
TextButton(
modifier = Modifier.heightIn(ThemeDimensions.touchable),
onClick = { dispatch(CreateLogin) }) {
Text(
text = stringResource(R.string.label_create_login),
- fontWeight = FontWeight.Bold,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 16.sp,
style = MaterialTheme.typography.bodyLarge
)
}
@@ -195,8 +222,9 @@ private fun InternetArchiveLoginContent(
Row(
modifier = Modifier
.fillMaxWidth()
+ .weight(1f)
.padding(top = ThemeDimensions.spacing.medium),
- verticalAlignment = Alignment.CenterVertically,
+ verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceEvenly
) {
TextButton(
@@ -204,9 +232,12 @@ private fun InternetArchiveLoginContent(
.weight(1f)
.heightIn(ThemeDimensions.touchable)
.padding(ThemeDimensions.spacing.small),
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = colorResource(R.color.colorOnBackground)
+ ),
shape = RoundedCornerShape(ThemeDimensions.roundedCorner),
onClick = { dispatch(Action.Cancel) }) {
- Text(stringResource(R.string.action_cancel))
+ Text(stringResource(R.string.back))
}
Button(
modifier = Modifier
@@ -219,7 +250,7 @@ private fun InternetArchiveLoginContent(
if (state.isBusy) {
CircularProgressIndicator(color = ThemeColors.material.primary)
} else {
- Text(stringResource(R.string.label_login))
+ Text(stringResource(R.string.next))
}
}
}
@@ -227,6 +258,7 @@ private fun InternetArchiveLoginContent(
}
@Composable
+@Preview
@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
private fun InternetArchiveLoginPreview() {
DefaultScaffoldPreview {
@@ -276,14 +308,11 @@ fun CustomTextField(
imeAction: ImeAction = ImeAction.Next,
) {
- TextField(
+ OutlinedTextField(
modifier = modifier.fillMaxWidth(),
value = value,
enabled = !isLoading,
onValueChange = onValueChange,
- label = {
- Text(label)
- },
placeholder = {
placeholder?.let {
Text(placeholder)
@@ -302,8 +331,10 @@ fun CustomTextField(
),
isError = isError,
colors = TextFieldDefaults.colors(
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
+ focusedContainerColor = MaterialTheme.colorScheme.background,
+ unfocusedContainerColor = MaterialTheme.colorScheme.background
+ //focusedIndicatorColor = Color.Transparent,
+ //unfocusedIndicatorColor = Color.Transparent,
),
)
}
@@ -325,14 +356,11 @@ fun CustomSecureField(
mutableStateOf(false)
}
- TextField(
+ OutlinedTextField(
modifier = modifier.fillMaxWidth(),
value = value,
enabled = !isLoading,
onValueChange = onValueChange,
- label = {
- Text(label)
- },
placeholder = {
Text(placeholder)
},
@@ -350,8 +378,10 @@ fun CustomSecureField(
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
isError = isError,
colors = TextFieldDefaults.colors(
- focusedIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent,
+ focusedContainerColor = MaterialTheme.colorScheme.background,
+ unfocusedContainerColor = MaterialTheme.colorScheme.background
+ //focusedIndicatorColor = Color.Transparent,
+ //unfocusedIndicatorColor = Color.Transparent,
),
trailingIcon = {
IconButton(
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/HomeActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/HomeActivity.kt
new file mode 100644
index 00000000..8fd1bd61
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/HomeActivity.kt
@@ -0,0 +1,220 @@
+package net.opendasharchive.openarchive.features.main
+
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import androidx.activity.compose.setContent
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.fragment.app.FragmentActivity
+import net.opendasharchive.openarchive.db.Project
+import net.opendasharchive.openarchive.features.main.ui.HomeScreen
+import net.opendasharchive.openarchive.features.main.ui.HomeViewModel
+import net.opendasharchive.openarchive.features.main.ui.SaveNavGraph
+import net.opendasharchive.openarchive.features.media.AddMediaType
+import net.opendasharchive.openarchive.features.media.MediaLaunchers
+import net.opendasharchive.openarchive.features.media.Picker
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import timber.log.Timber
+
+class HomeActivity: FragmentActivity() {
+
+ private val viewModel by viewModel()
+
+ // We'll hold a reference to the media launchers registered with Picker.
+ private lateinit var mediaLaunchers: MediaLaunchers
+
+ private val mNewFolderResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ //TODO: Refresh projects in MainViewModel
+ }
+ }
+
+ private val folderResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK) {
+ val selectedFolderId:Long? = result.data?.getLongExtra("SELECTED_FOLDER_ID", -1)
+ if (selectedFolderId != null && selectedFolderId > -1) {
+ navigateToFolder(selectedFolderId)
+ }
+ }
+ }
+
+ private val requestPermissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted: Boolean ->
+ if (isGranted) {
+ Timber.d("Able to post notifications")
+ } else {
+ Timber.d("Need to explain")
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ installSplashScreen()
+
+ // Perform any intent processing (e.g. deep-links or shared media)
+ handleIntent(intent)
+
+ // Check notification permission (for Android 13+)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ checkNotificationPermissions()
+ }
+
+ // Get a reference to a view to serve as the root for Snackbars, etc.
+ val rootView: View = findViewById(android.R.id.content)
+
+ // Register media launchers via Picker.
+ // The lambda for 'project' should return the currently selected project.
+ // For now, this stub returns null—you should wire it to your actual selection.
+ mediaLaunchers = Picker.register(
+ activity = this,
+ root = rootView,
+ project = { getCurrentProject() },
+ completed = { media ->
+ // For example, refresh the current project UI and preview media.
+ refreshCurrentProject()
+ if (media.isNotEmpty()) {
+ previewMedia()
+ }
+ }
+ )
+
+ // Set up your Compose UI and pass callbacks.
+ setContent {
+ SaveNavGraph(
+ context = this@HomeActivity,
+ onExit = {
+ finish()
+ },
+ viewModel = viewModel,
+ onNewFolder = { launchNewFolder() },
+ onFolderSelected = { folderId -> navigateToFolder(folderId) },
+ onAddMedia = { mediaType -> addMediaClicked(mediaType) }
+ )
+ }
+ }
+
+ /**
+ * Returns the currently selected project.
+ * Replace this stub with your actual project–retrieval logic.
+ */
+ private fun getCurrentProject(): Project? {
+ // TODO: Return your current project from a ViewModel or other state.
+ return null
+ }
+
+ /**
+ * Refresh UI details for the current project.
+ */
+ private fun refreshCurrentProject() {
+ // TODO: Update your UI state, refresh fragment content, etc.
+ }
+
+ /**
+ * Launch a preview after media import.
+ */
+ private fun previewMedia() {
+ // TODO: Launch your preview activity or update the UI as needed.
+ }
+
+ /**
+ * Launch the AddFolderActivity using your folder launcher.
+ */
+ private fun launchNewFolder() {
+ // Example: startActivity(Intent(this, AddFolderActivity::class.java))
+ // Or, if you have a registered launcher, use it here.
+ }
+
+ /**
+ * Navigate to a folder after selection.
+ */
+ private fun navigateToFolder(folderId: Long) {
+ // TODO: Update your navigation or fragment state to display the selected folder.
+ }
+
+ /**
+ * Handle "Add Media" events from the Compose UI.
+ */
+ private fun addMediaClicked(mediaType: AddMediaType) {
+ if (getCurrentProject() != null) {
+ // If you wish to show hints or dialogs before picking media,
+ // insert that logic here (e.g., check Prefs.addMediaHint).
+ when (mediaType) {
+ AddMediaType.CAMERA -> {
+ // Launch the camera using Picker.
+ Picker.takePhoto(this, mediaLaunchers.cameraLauncher)
+ }
+ AddMediaType.GALLERY -> {
+ // Launch the gallery/image picker.
+ Picker.pickMedia(this, mediaLaunchers.imagePickerLauncher)
+ }
+ AddMediaType.FILES -> {
+ // Launch the file picker.
+ Picker.pickFiles(mediaLaunchers.filePickerLauncher)
+ }
+ }
+ } else {
+ // If no project is selected, prompt the user to create one (e.g. add a folder).
+ launchNewFolder()
+ }
+ }
+
+ /**
+ * Check for POST_NOTIFICATIONS permission on Android 13+.
+ */
+ private fun checkNotificationPermissions() {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ Timber.d("Notification permission already granted")
+ } else if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
+ showNotificationPermissionRationale()
+ } else {
+ requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ }
+
+ /**
+ * Show a rationale for notification permission.
+ */
+ private fun showNotificationPermissionRationale() {
+ // TODO: Display a dialog or Snackbar explaining why notifications are needed.
+ Timber.d("Showing notification permission rationale")
+ }
+
+ /**
+ * Handle incoming intents for deep-linking, shared media, etc.
+ */
+ private fun handleIntent(intent: Intent?) {
+ intent?.let { receivedIntent ->
+ when (receivedIntent.action) {
+ Intent.ACTION_VIEW -> {
+ val uri = receivedIntent.data
+ if (uri?.scheme == "save-veilid") {
+ processUri(uri)
+ }
+ }
+ // Optionally handle other actions (like ACTION_SEND) here.
+ }
+ }
+ }
+
+ private fun processUri(uri: Uri) {
+ // Process the URI similarly to your original logic.
+ Timber.d("Processing URI: $uri")
+ // TODO: Extract path, query parameters, etc.
+ }
+
+
+}
\ No newline at end of file
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 325c96f4..bb9a18d2 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
@@ -1,36 +1,65 @@
package net.opendasharchive.openarchive.features.main
import android.Manifest
+import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.graphics.Point
+import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
+import android.view.Gravity
+import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputMethodManager
+import android.widget.LinearLayout
+import android.widget.PopupWindow
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.compose.material3.MaterialTheme
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import net.opendasharchive.openarchive.BuildConfig
import net.opendasharchive.openarchive.R
import net.opendasharchive.openarchive.core.logger.AppLogger
import net.opendasharchive.openarchive.databinding.ActivityMainBinding
+import net.opendasharchive.openarchive.databinding.PopupFolderOptionsBinding
+import net.opendasharchive.openarchive.db.Media
import net.opendasharchive.openarchive.db.Project
import net.opendasharchive.openarchive.db.Space
+import net.opendasharchive.openarchive.extensions.getMeasurments
import net.opendasharchive.openarchive.features.core.BaseActivity
+import net.opendasharchive.openarchive.features.core.UiImage
+import net.opendasharchive.openarchive.features.core.UiText
import net.opendasharchive.openarchive.features.core.asUiImage
import net.opendasharchive.openarchive.features.core.asUiText
+import net.opendasharchive.openarchive.features.core.dialog.ButtonData
+import net.opendasharchive.openarchive.features.core.dialog.DialogConfig
+import net.opendasharchive.openarchive.features.core.dialog.DialogType
+import net.opendasharchive.openarchive.features.core.dialog.showDialog
import net.opendasharchive.openarchive.features.core.dialog.showInfoDialog
import net.opendasharchive.openarchive.features.folders.AddFolderActivity
+import net.opendasharchive.openarchive.features.main.adapters.FolderDrawerAdapter
+import net.opendasharchive.openarchive.features.main.adapters.FolderDrawerAdapterListener
+import net.opendasharchive.openarchive.features.main.adapters.SpaceDrawerAdapter
+import net.opendasharchive.openarchive.features.main.adapters.SpaceDrawerAdapterListener
import net.opendasharchive.openarchive.features.media.AddMediaDialogFragment
import net.opendasharchive.openarchive.features.media.AddMediaType
import net.opendasharchive.openarchive.features.media.ContentPickerFragment
@@ -39,47 +68,67 @@ import net.opendasharchive.openarchive.features.media.Picker
import net.opendasharchive.openarchive.features.media.PreviewActivity
import net.opendasharchive.openarchive.features.onboarding.Onboarding23Activity
import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity
-import net.opendasharchive.openarchive.features.settings.FoldersActivity
+import net.opendasharchive.openarchive.features.onboarding.StartDestination
import net.opendasharchive.openarchive.features.settings.passcode.AppConfig
-import net.opendasharchive.openarchive.features.spaces.SpacesActivity
import net.opendasharchive.openarchive.services.snowbird.SnowbirdBridge
import net.opendasharchive.openarchive.services.snowbird.service.SnowbirdService
+import net.opendasharchive.openarchive.upload.UploadManagerFragment
import net.opendasharchive.openarchive.upload.UploadService
-import net.opendasharchive.openarchive.util.AlertHelper
import net.opendasharchive.openarchive.util.Prefs
import net.opendasharchive.openarchive.util.ProofModeHelper
import net.opendasharchive.openarchive.util.Utility
+import net.opendasharchive.openarchive.util.extensions.Position
import net.opendasharchive.openarchive.util.extensions.cloak
import net.opendasharchive.openarchive.util.extensions.hide
+import net.opendasharchive.openarchive.util.extensions.scaleAndTintDrawable
+import net.opendasharchive.openarchive.util.extensions.scaled
import net.opendasharchive.openarchive.util.extensions.show
import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
import java.text.NumberFormat
-class MainActivity : BaseActivity() {
+class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAdapterListener {
private val appConfig by inject()
+ private val viewModel by viewModel()
private var mMenuDelete: MenuItem? = null
private var mSnackBar: Snackbar? = null
+ var uploadManagerFragment: UploadManagerFragment? = null
+
private lateinit var binding: ActivityMainBinding
private lateinit var mPagerAdapter: ProjectAdapter
+ private lateinit var mSpaceAdapter: SpaceDrawerAdapter
+ private lateinit var mFolderAdapter: FolderDrawerAdapter
private lateinit var mediaLaunchers: MediaLaunchers
- private var mLastItem: Int = 0
- private var mLastMediaItem: Int = 0
+ private var mSelectedPageIndex: Int = 0
+ private var mSelectedMediaPageIndex: Int = 0
+ private var serverListOffset: Float = 0F
+ private var serverListCurOffset: Float = 0F
+
+ private var selectModeToggle: Boolean = false
+ private var currentSelectionCount = 0
+
+ private enum class FolderBarMode { INFO, SELECTION, EDIT }
+ // Hold the current mode (default to INFO)
+ private var folderBarMode = FolderBarMode.INFO
+
+ // Current page getter/setter (updates bottom navbar accordingly)
private var mCurrentPagerItem
- get() = binding.pager.currentItem
+ get() = binding.contentMain.pager.currentItem
set(value) {
- binding.pager.currentItem = value
+ binding.contentMain.pager.currentItem = value
updateBottomNavbar(value)
}
+ // ----- Activity Result Launchers & Permission Launcher -----
private val mNewFolderResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
@@ -87,16 +136,6 @@ class MainActivity : BaseActivity() {
}
}
- private val folderResultLauncher =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- val selectedFolderId = result.data?.getLongExtra("SELECTED_FOLDER_ID", -1)
- if (selectedFolderId != null && selectedFolderId > -1) {
- navigateToFolder(selectedFolderId)
- }
- }
- }
-
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
@@ -110,256 +149,577 @@ class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
+ ///enableEdgeToEdge()
+
super.onCreate(savedInstanceState)
installSplashScreen()
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+// window.insetsController?.let {
+// it.hide(WindowInsets.Type.statusBars())
+// it.hide(WindowInsets.Type.systemBars())
+// it.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+// }
+// } else {
+// // For older versions, use the deprecated approach
+// window.setFlags(
+// WindowManager.LayoutParams.FLAG_FULLSCREEN,
+// WindowManager.LayoutParams.FLAG_FULLSCREEN
+// )
+// }
+
+// window.apply {
+// clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+// addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+// statusBarColor = ContextCompat.getColor(this@MainActivity, R.color.colorPrimary)
+// // optional. if you want the icons to be light.
+// decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+// }
+
+
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
+ viewModel.log("MainActivity onCreate called")
+
+ initMediaLaunchers()
+ setupToolbarAndPager()
+ setupNavigationDrawer()
+ setupBottomNavBar()
+ setupFolderBar()
+ setupBottomSheetObserver()
+
+
+ if (appConfig.isDwebEnabled) {
+ checkNotificationPermissions()
+ SnowbirdBridge.getInstance().initialize()
+ startForegroundService(Intent(this, SnowbirdService::class.java))
+ handleIntent(intent)
+ }
+
+
+ if (BuildConfig.DEBUG) {
+ binding.contentMain.imgLogo.setOnLongClickListener {
+ startActivity(Intent(this, HomeActivity::class.java))
+ true
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ AppLogger.i("MainActivity onResume is called.......")
+ refreshSpace()
+ mCurrentPagerItem = mSelectedPageIndex
+ if (!Prefs.didCompleteOnboarding) {
+ startActivity(Intent(this, Onboarding23Activity::class.java))
+ }
+ importSharedMedia(intent)
+ if (serverListOffset == 0F) {
+ val dims = binding.spaces.getMeasurments()
+ serverListOffset = -dims.second.toFloat()
+ serverListCurOffset = serverListOffset
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun onStart() {
+ super.onStart()
+
+ if(Prefs.useProofMode){
+ Prefs.proofModeLocation = true
+ Prefs.proofModeNetwork = true
+ }else{
+ Prefs.proofModeLocation = false
+ Prefs.proofModeNetwork = false
+ }
+
+ ProofModeHelper.init(this) {
+ // Check for any queued uploads and restart, only after ProofMode is correctly initialized.
+ UploadService.startUploadService(this)
+ }
+ }
+
+ // ----- Initialization Methods -----
+ private fun initMediaLaunchers() {
mediaLaunchers = Picker.register(
activity = this,
root = binding.root,
project = { getSelectedProject() },
completed = { media ->
refreshCurrentProject()
+ if (media.isNotEmpty()) navigateToPreview()
+ }
+ )
+ }
- if (media.isNotEmpty()) {
- preview()
- }
- })
-
- setSupportActionBar(binding.toolbar)
- supportActionBar?.setDisplayHomeAsUpEnabled(false)
- supportActionBar?.title = null
+ private fun setupToolbarAndPager() {
+ setSupportActionBar(binding.contentMain.toolbar)
+ supportActionBar?.apply {
+ setDisplayHomeAsUpEnabled(false)
+ title = null
+ }
mPagerAdapter = ProjectAdapter(supportFragmentManager, lifecycle)
- binding.pager.adapter = mPagerAdapter
-
- binding.pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
- override fun onPageScrolled(
- position: Int, positionOffset: Float,
- positionOffsetPixels: Int
- ) {
- // Do Nothing
- }
+ binding.contentMain.pager.adapter = mPagerAdapter
+ binding.contentMain.pager.registerOnPageChangeCallback(object :
+ ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
- mLastItem = position
+ mSelectedPageIndex = position
if (position < mPagerAdapter.settingsIndex) {
- mLastMediaItem = position
+ mSelectedMediaPageIndex = position
+ val selectedProject = getSelectedProject()
+ mFolderAdapter.updateSelectedProject(selectedProject)
+ }
+ if (!appConfig.multipleProjectSelectionMode) {
+ getCurrentMediaFragment()?.cancelSelection()
}
-
updateBottomNavbar(position)
-
refreshCurrentProject()
}
+ })
+ }
+
+ private fun setupNavigationDrawer() {
+ // Drawer listener resets state on close
+ binding.root.addDrawerListener(object : DrawerLayout.DrawerListener {
+ override fun onDrawerClosed(drawerView: View) {
+ collapseSpacesList()
+ }
- override fun onPageScrollStateChanged(state: Int) {}
+ override fun onDrawerOpened(drawerView: View) {
+ //
+ }
+
+ override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
+ //
+ }
+
+ override fun onDrawerStateChanged(newState: Int) {
+ //
+ }
})
- setupBottomNavBar()
+ binding.navigationDrawerHeader.setOnClickListener { toggleSpacesList() }
+ binding.dimOverlay.setOnClickListener { collapseSpacesList() }
+ mSpaceAdapter = SpaceDrawerAdapter(this)
+ binding.spaces.layoutManager = LinearLayoutManager(this)
+ binding.spaces.adapter = mSpaceAdapter
- binding.breadcrumbSpace.setOnClickListener {
- startActivity(Intent(this, SpacesActivity::class.java))
- }
+ mFolderAdapter = FolderDrawerAdapter(this)
+ binding.folders.layoutManager = LinearLayoutManager(this)
+ binding.folders.adapter = mFolderAdapter
- binding.breadcrumbFolder.setOnClickListener {
- val selectedSpaceId = getSelectedSpace()?.id
- val selectedProjectId = getSelectedProject()?.id
- val intent = Intent(this, FoldersActivity::class.java)
- intent.putExtra(
- FoldersActivity.EXTRA_SELECTED_SPACE_ID,
- selectedSpaceId
- ) // Pass the selected space ID
- intent.putExtra(
- FoldersActivity.EXTRA_SELECTED_PROJECT_ID,
- selectedProjectId
- ) // Pass the selected project ID
- folderResultLauncher.launch(intent)
+ binding.btnAddFolder.scaleAndTintDrawable(Position.Start, 0.75)
+ binding.btnAddFolder.setOnClickListener {
+ closeDrawer()
+ navigateToAddFolder()
}
+ updateCurrentSpaceAtDrawer()
+ }
- if (appConfig.snowbirdEnabled) {
+ private fun setupBottomNavBar() {
+ with(binding.contentMain.bottomNavBar) {
+ onMyMediaClick = {
+ mCurrentPagerItem = mSelectedMediaPageIndex
+ }
+ onAddClick = { addClicked(AddMediaType.GALLERY) }
+ onSettingsClick = {
+ mCurrentPagerItem = mPagerAdapter.settingsIndex
+ }
- checkNotificationPermissions()
+ if (Picker.canPickFiles(this@MainActivity)) {
+ setAddButtonLongClickEnabled()
+ onAddLongClick = {
+ val addMediaBottomSheet =
+ ContentPickerFragment { actionType -> addClicked(actionType) }
+ addMediaBottomSheet.show(supportFragmentManager, ContentPickerFragment.TAG)
+ }
+ supportFragmentManager.setFragmentResultListener(
+ AddMediaDialogFragment.RESP_TAKE_PHOTO, this@MainActivity
+ ) { _, _ -> addClicked(AddMediaType.CAMERA) }
+ supportFragmentManager.setFragmentResultListener(
+ AddMediaDialogFragment.RESP_PHOTO_GALLERY, this@MainActivity
+ ) { _, _ -> addClicked(AddMediaType.GALLERY) }
+ supportFragmentManager.setFragmentResultListener(
+ AddMediaDialogFragment.RESP_FILES, this@MainActivity
+ ) { _, _ -> addClicked(AddMediaType.FILES) }
+ }
+ }
+ }
- SnowbirdBridge.getInstance().initialize()
- val intent = Intent(this, SnowbirdService::class.java)
- startForegroundService(intent)
+ private fun setupFolderBar() {
+ // Tapping the edit button shows the folder options popup.
+ binding.contentMain.btnEdit.setOnClickListener { btnView ->
+ val location = IntArray(2)
+ binding.contentMain.btnEdit.getLocationOnScreen(location)
+ val point = Point(location[0], location[1])
+ showFolderOptionsPopup(point)
+ }
+ // In selection mode, cancel selection reverts to INFO mode.
+ binding.contentMain.btnCancelSelection.setOnClickListener {
+ setFolderBarMode(FolderBarMode.INFO)
+ getCurrentMediaFragment()?.cancelSelection()
+ }
+ // In the edit (rename) container, cancel button reverts to INFO mode.
+ binding.contentMain.btnCancelEdit.setOnClickListener {
+ setFolderBarMode(FolderBarMode.INFO)
+ }
+ // Listen for the "done" action to commit a rename.
+ binding.contentMain.etFolderName.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ val newName = binding.contentMain.etFolderName.text.toString().trim()
+ if (newName.isNotEmpty()) {
+ renameCurrentFolder(newName)
+ setFolderBarMode(FolderBarMode.INFO)
+ } else {
+ Snackbar.make(
+ binding.root,
+ "Folder name cannot be empty",
+ Snackbar.LENGTH_SHORT
+ ).show()
+ }
+ // Hide the keyboard
+ val imm =
+ binding.contentMain.etFolderName.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.hideSoftInputFromWindow(binding.contentMain.etFolderName.windowToken, 0)
- handleIntent(intent)
+ // Remove focus from the EditText
+ binding.contentMain.etFolderName.clearFocus()
+
+ true
+ } else false
}
- }
- private fun handleIntent(intent: Intent) {
- if (intent.action == Intent.ACTION_VIEW) {
- val uri = intent.data
- if (uri?.scheme == "save-veilid") {
- processUri(uri)
- }
+ binding.contentMain.btnRemoveSelected.setOnClickListener {
+ showDeleteSelectedMediaConfirmDialog()
}
}
- private fun processUri(uri: Uri) {
- val path = uri.path
- val queryParams = uri.queryParameterNames.associateWith { uri.getQueryParameter(it) }
- AppLogger.d("Path: $path, QueryParams: $queryParams")
+ // Called when a new folder name is confirmed. (Adjust as needed to update your data store.)
+ private fun renameCurrentFolder(newName: String) {
+ val project = getSelectedProject()
+ project?.let {
+ it.description = newName
+ it.save()
+ refreshCurrentProject()
+ Snackbar.make(binding.root, "Folder renamed", Snackbar.LENGTH_SHORT).show()
+ }
}
- private fun setupBottomNavBar() {
- binding.bottomNavBar.onMyMediaClick = {
- mCurrentPagerItem = mLastMediaItem
+ private fun showFolderOptionsPopup(p: Point) {
+ val layoutInflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
+ val popupBinding = PopupFolderOptionsBinding.inflate(layoutInflater)
+ val popup = PopupWindow(this).apply {
+ contentView = popupBinding.root
+ width = LinearLayout.LayoutParams.WRAP_CONTENT
+ height = LinearLayout.LayoutParams.WRAP_CONTENT
+ isFocusable = true
+ setBackgroundDrawable(ColorDrawable())
+ animationStyle = R.style.popup_window_animation
}
- binding.bottomNavBar.onAddClick = {
- addClicked(AddMediaType.GALLERY)
+ // Option to toggle selection mode
+ popupBinding.menuFolderBarSelectMedia.setOnClickListener {
+ popup.dismiss()
+ setFolderBarMode(FolderBarMode.SELECTION)
+ }
+ // Rename folder
+ popupBinding.menuFolderBarRenameFolder.setOnClickListener {
+ popup.dismiss()
+ setFolderBarMode(FolderBarMode.EDIT)
}
- binding.bottomNavBar.onSettingsClick = {
- mCurrentPagerItem = mPagerAdapter.settingsIndex
+ // Remove folder
+ popupBinding.menuFolderBarRemove.setOnClickListener {
+ popup.dismiss()
+ if (getSelectedProject() != null) {
+ showDeleteFolderConfirmDialog()
+ } else {
+ Snackbar.make(binding.root, "Folder not found", Snackbar.LENGTH_LONG).show()
+ }
}
- if (Picker.canPickFiles(this)) {
- binding.bottomNavBar.setAddButtonLongClickEnabled()
+ // Adjust popup position if needed
+ val x = 200
+ val y = 60
+ popup.showAtLocation(binding.root, Gravity.NO_GRAVITY, p.x + x, p.y + y)
+ }
- binding.bottomNavBar.onAddLongClick = {
- //val addMediaDialogFragment = AddMediaDialogFragment()
- //addMediaDialogFragment.show(supportFragmentManager, addMediaDialogFragment.tag)
+ fun setSelectionMode(isSelecting: Boolean) {
+ if (isSelecting) {
+ setFolderBarMode(FolderBarMode.SELECTION)
+ } else {
+ setFolderBarMode(FolderBarMode.INFO)
+ }
+ }
- val addMediaBottomSheet =
- ContentPickerFragment { actionType -> addClicked(actionType) }
- addMediaBottomSheet.show(supportFragmentManager, ContentPickerFragment.TAG)
- }
+ // New helper: update the cancel selection TextView to show the number of selected items.
+ fun updateSelectedCount(count: Int) {
+ // For example, if count > 0 display “Selected: X”; otherwise, revert to “Select Media”.
+ //binding.contentMain.tvSelectedCount.text = if (count > 0) "Selected: $count" else "Select Media"
+ }
- supportFragmentManager.setFragmentResultListener(
- AddMediaDialogFragment.RESP_TAKE_PHOTO,
- this
- ) { _, _ ->
- addClicked(AddMediaType.CAMERA)
- }
- supportFragmentManager.setFragmentResultListener(
- AddMediaDialogFragment.RESP_PHOTO_GALLERY,
- this
- ) { _, _ ->
- addClicked(AddMediaType.GALLERY)
+ private fun showDeleteSelectedMediaConfirmDialog() {
+ dialogManager.showDialog(
+ config = DialogConfig(
+ type = DialogType.Warning,
+ title = R.string.menu_delete.asUiText(),
+ message = R.string.menu_delete_desc.asUiText(),
+ icon = UiImage.DrawableResource(R.drawable.ic_trash),
+ positiveButton = ButtonData(
+ text = R.string.lbl_ok.asUiText(),
+ action = {
+ getCurrentMediaFragment()?.deleteSelected()
+ updateSelectedCount(0)
+ }
+ ),
+ neutralButton =
+ ButtonData(
+ text = UiText.StringResource(R.string.lbl_Cancel),
+ action = {
+
+ }
+ )
+ )
+ )
+ }
+
+ private fun showDeleteFolderConfirmDialog() {
+ dialogManager.showDialog(dialogManager.requireResourceProvider()) {
+ type = DialogType.Error
+ icon = UiImage.DrawableResource(R.drawable.ic_trash)
+ title = UiText.StringResource(R.string.remove_from_app)
+ message = UiText.StringResource(R.string.action_remove_project)
+ destructiveButton {
+ text = UiText.StringResource(R.string.remove)
+ action = {
+ getSelectedProject()?.delete()
+ refreshProjects()
+ updateCurrentFolderVisibility()
+ refreshCurrentProject()
+ Snackbar.make(binding.root, "Folder removed", Snackbar.LENGTH_SHORT).show()
+ }
}
- supportFragmentManager.setFragmentResultListener(
- AddMediaDialogFragment.RESP_FILES,
- this
- ) { _, _ ->
- addClicked(AddMediaType.FILES)
+ neutralButton {
+ text = UiText.StringResource(R.string.lbl_Cancel)
+ action = {
+ dialogManager.dismissDialog()
+ }
}
}
}
- private fun updateBottomNavbar(position: Int) {
- binding.bottomNavBar.updateSelectedItem(isSettings = position == mPagerAdapter.settingsIndex)
- if (position == mPagerAdapter.settingsIndex) {
- binding.breadcrumbContainer.hide()
+ private fun getCurrentMediaFragment(): MainMediaFragment? {
+ val currentItem = binding.contentMain.pager.currentItem
+ return supportFragmentManager.findFragmentByTag("f$currentItem") as? MainMediaFragment
+ }
+
+
+ // ----- Drawer Helpers -----
+ private fun toggleDrawerState() {
+ if (binding.root.isDrawerOpen(binding.drawerContent)) {
+ closeDrawer()
} else {
- // Show the breadcrumb container only if there's any server available
- if (Space.current != null) {
- binding.breadcrumbContainer.show()
- }
+ openDrawer()
}
}
- @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
- override fun onStart() {
- super.onStart()
+ private fun openDrawer() {
+ binding.root.openDrawer(binding.drawerContent)
+ }
- if(Prefs.useProofMode){
- Prefs.proofModeLocation = true
- Prefs.proofModeNetwork = true
- }else{
- Prefs.proofModeLocation = false
- Prefs.proofModeNetwork = false
- }
+ private fun closeDrawer() {
+ binding.root.closeDrawer(binding.drawerContent)
+ }
- ProofModeHelper.init(this) {
- // Check for any queued uploads and restart, only after ProofMode is correctly initialized.
- UploadService.startUploadService(this)
+ private fun toggleSpacesList() {
+ if (serverListCurOffset == serverListOffset) {
+ expandSpacesList()
+ } else {
+ collapseSpacesList()
}
}
- override fun onResume() {
- super.onResume()
+ private fun expandSpacesList() {
+ serverListCurOffset = 0f
+ binding.spaceListMore.setImageDrawable(
+ ContextCompat.getDrawable(this, R.drawable.ic_expand_less)
+ )
+ binding.spaces.visibility = View.VISIBLE
+ binding.dimOverlay.visibility = View.VISIBLE
+ binding.spaces.bringToFront()
+ binding.dimOverlay.bringToFront()
+ binding.spaces.animate()
+ .translationY(0f).alpha(1f).setDuration(200)
+ .withStartAction {
+ binding.spacesHeaderSeparator.alpha = 0.3f
+ binding.folders.alpha = 0.3f
+ binding.btnAddFolder.alpha = 0.3f
+ }
+ binding.dimOverlay.animate().alpha(1f).setDuration(200)
+ binding.navigationDrawerHeader.elevation = 8f
+ }
- refreshSpace()
+ private fun collapseSpacesList() {
+ serverListCurOffset = serverListOffset
+ binding.spaceListMore.setImageDrawable(
+ ContextCompat.getDrawable(this, R.drawable.ic_expand_more)
+ )
+
+ binding.spaces.animate()
+ .translationY(serverListOffset).alpha(0f).setDuration(200)
+ .withEndAction {
+ binding.spaces.visibility = View.GONE
+ binding.dimOverlay.visibility = View.GONE
+ binding.spacesHeaderSeparator.alpha = 1f
+ binding.folders.alpha = 1f
+ binding.btnAddFolder.alpha = 1f
+ }
+ binding.dimOverlay.animate().alpha(0f).setDuration(200)
+ binding.navigationDrawerHeader.elevation = 0f
+ }
- mCurrentPagerItem = mLastItem
+ private fun updateCurrentSpaceAtDrawer() {
+ Space.current?.setAvatar(binding.currentSpaceIcon)
+ mSpaceAdapter.notifyDataSetChanged()
- if (!Prefs.didCompleteOnboarding) {
- startActivity(Intent(this, Onboarding23Activity::class.java))
+ if (Space.current == null) {
+ binding.btnAddFolder.visibility = View.INVISIBLE
+ } else {
+ binding.btnAddFolder.visibility = View.VISIBLE
}
-
- importSharedMedia(intent)
}
+ // ----- Refresh & Update Methods -----
+ /**
+ * Updates the visibility of the current folder container.
+ * The container is only visible if:
+ * 1. We are not on the settings page AND
+ * 2. There is a current space with at least one project.
+ */
+ // Central function to update folder bar state
+ private fun setFolderBarMode(mode: FolderBarMode) {
+ folderBarMode = mode
+ when (mode) {
+ FolderBarMode.INFO -> {
+ binding.contentMain.folderInfoContainer.visibility = View.VISIBLE
+ binding.contentMain.folderSelectionContainer.visibility = View.GONE
+ binding.contentMain.folderEditContainer.visibility = View.GONE
+
+ if (Space.current != null) {
+ if (Space.current?.projects?.isNotEmpty() == true) {
+ binding.contentMain.folderInfoContainerRight.visibility = View.VISIBLE
+ } else {
+ binding.contentMain.folderInfoContainerRight.visibility = View.INVISIBLE
+ }
+ } else {
+ binding.contentMain.folderInfoContainerRight.visibility = View.INVISIBLE
+ }
+ }
- private fun navigateToFolder(folderId: Long) {
- val folderIndex = mPagerAdapter.getProjectIndexById(folderId)
- if (folderIndex >= 0) {
- binding.pager.setCurrentItem(folderIndex, true)
- mCurrentPagerItem = folderIndex
+ FolderBarMode.SELECTION -> {
+ binding.contentMain.folderInfoContainer.visibility = View.GONE
+ binding.contentMain.folderSelectionContainer.visibility = View.VISIBLE
+ binding.contentMain.folderEditContainer.visibility = View.GONE
+ }
- } else {
- Toast.makeText(this, "Folder not found", Toast.LENGTH_SHORT).show()
+ FolderBarMode.EDIT -> {
+ binding.contentMain.folderInfoContainer.visibility = View.GONE
+ binding.contentMain.folderSelectionContainer.visibility = View.GONE
+ binding.contentMain.folderEditContainer.visibility = View.VISIBLE
+ // Prepopulate the rename field with the current folder name
+ binding.contentMain.etFolderName.setText(getSelectedProject()?.description ?: "")
+ binding.contentMain.etFolderName.requestFocus()
+
+ // Show the keyboard
+ val imm =
+ binding.contentMain.etFolderName.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.showSoftInput(
+ binding.contentMain.etFolderName,
+ InputMethodManager.SHOW_IMPLICIT
+ )
+ }
}
}
+ private fun updateCurrentFolderVisibility() {
+ val currentPagerIndex = binding.contentMain.pager.currentItem
+ val settingsIndex = mPagerAdapter.settingsIndex
+ if (currentPagerIndex == settingsIndex) {
+ binding.contentMain.folderBar.hide()
+ // Reset to default mode
+ setFolderBarMode(FolderBarMode.INFO)
+
+ // Force ViewPager2 to re-measure its layout after visibility change
+ binding.contentMain.pager.post {
+ binding.contentMain.pager.requestLayout()
+ }
+ } else {
+ binding.contentMain.folderBar.show()
+ setFolderBarMode(FolderBarMode.INFO)
+ }
- fun updateAfterDelete(done: Boolean) {
- mMenuDelete?.isVisible = !done
-
- if (done) refreshCurrentFolderCount()
+ mFolderAdapter.notifyDataSetChanged()
}
- private fun addFolder() {
- mNewFolderResultLauncher.launch(Intent(this, AddFolderActivity::class.java))
+ private fun updateBottomNavbar(position: Int) {
+ val isSettings = position == mPagerAdapter.settingsIndex
+ binding.contentMain.bottomNavBar.updateSelectedItem(isSettings = isSettings)
+ updateCurrentFolderVisibility()
+ invalidateOptionsMenu()
}
private fun refreshSpace() {
val currentSpace = Space.current
- currentSpace?.let { space ->
- binding.breadcrumbSpace.text = space.friendlyName
- space.setAvatar(binding.spaceIcon)
- } ?: run {
- binding.breadcrumbContainer.visibility = View.INVISIBLE
+ if (currentSpace != null) {
+ binding.spaceNameLayout.visibility = View.VISIBLE
+ binding.currentSpaceName.text = currentSpace.friendlyName
+ updateCurrentSpaceAtDrawer()
+ currentSpace.setAvatar(binding.contentMain.spaceIcon)
+ } else {
+ binding.contentMain.spaceIcon.visibility = View.INVISIBLE
+ binding.spaceNameLayout.visibility = View.INVISIBLE
}
+ mSpaceAdapter.update(Space.getAll().asSequence().toList())
+ updateCurrentSpaceAtDrawer()
refreshProjects()
+ refreshCurrentProject()
+ updateCurrentFolderVisibility()
}
private fun refreshProjects(setProjectId: Long? = null) {
val projects = Space.current?.projects ?: emptyList()
-
mPagerAdapter.updateData(projects)
-
- binding.pager.adapter = mPagerAdapter
+ binding.contentMain.pager.adapter = mPagerAdapter
setProjectId?.let {
mCurrentPagerItem = mPagerAdapter.getProjectIndexById(it, default = 0)
}
+ mFolderAdapter.update(projects)
}
private fun refreshCurrentProject() {
val project = getSelectedProject()
if (project != null) {
- binding.pager.post {
+ binding.contentMain.pager.post {
mPagerAdapter.notifyProjectChanged(project)
}
-
- project.space?.setAvatar(binding.spaceIcon)
- binding.breadcrumbFolder.text = project.description
- binding.breadcrumbFolder.show()
-
+ binding.contentMain.folderInfoContainer.visibility = View.VISIBLE
+ project.space?.setAvatar(binding.contentMain.spaceIcon)
+ binding.contentMain.folderName.text = project.description
+ binding.contentMain.folderNameArrow.visibility = View.VISIBLE
+ binding.contentMain.folderName.visibility = View.VISIBLE
} else {
- this@MainActivity.binding.breadcrumbFolder.cloak()
+ binding.contentMain.folderNameArrow.visibility = View.INVISIBLE
+ binding.contentMain.folderName.visibility = View.INVISIBLE
}
-
+ updateCurrentFolderVisibility()
refreshCurrentFolderCount()
}
@@ -367,57 +727,157 @@ class MainActivity : BaseActivity() {
val project = getSelectedProject()
if (project != null) {
- val count = NumberFormat.getInstance().format(
- project.collections.map { it.size }
- .reduceOrNull { acc, count -> acc + count } ?: 0)
+ val count = project.collections.map { it.size }
+ .reduceOrNull { acc, count -> acc + count } ?: 0
+ binding.contentMain.itemCount.text = NumberFormat.getInstance().format(count)
+ if (!selectModeToggle) {
+ binding.contentMain.itemCount.show()
+ }
+ } else {
+ binding.contentMain.itemCount.cloak()
+ }
+ }
- binding.folderCount.text = count
- binding.folderCount.show()
+ // ----- Navigation & Media Handling -----
+ private fun navigateToAddServer() {
+ closeDrawer()
+ startActivity(Intent(this, SpaceSetupActivity::class.java))
+ }
+ private fun navigateToAddFolder() {
+ val intent = Intent(this, SpaceSetupActivity::class.java)
+ if (Space.current?.tType == Space.Type.INTERNET_ARCHIVE) {
+ // We cannot browse the Internet Archive. Directly forward to creating a project,
+ // as it doesn't make sense to show a one-option menu.
+ intent.putExtra("start_destination", StartDestination.ADD_NEW_FOLDER.name)
} else {
- binding.folderCount.cloak()
+ intent.putExtra("start_destination", StartDestination.ADD_FOLDER.name)
+ }
+ mNewFolderResultLauncher.launch(intent)
+// mNewFolderResultLauncher.launch(Intent(this, AddFolderActivity::class.java))
+ }
+
+ private fun addClicked(mediaType: AddMediaType) {
+
+ when {
+ getSelectedProject() != null -> {
+ if (Prefs.addMediaHint) {
+ when (mediaType) {
+ AddMediaType.CAMERA -> Picker.takePhoto(this, mediaLaunchers.cameraLauncher)
+ AddMediaType.GALLERY -> Picker.pickMedia(
+ this,
+ mediaLaunchers.imagePickerLauncher
+ )
+
+ AddMediaType.FILES -> Picker.pickFiles(mediaLaunchers.filePickerLauncher)
+ }
+ } else {
+ dialogManager.showInfoDialog(
+ icon = R.drawable.perm_media_24px.asUiImage(),
+ title = R.string.press_and_hold_options_media_screen_title.asUiText(),
+ message = R.string.press_and_hold_options_media_screen_message.asUiText(),
+ onDone = {
+ Prefs.addMediaHint = true
+ }
+ )
+ }
+ }
+
+ Space.current == null -> navigateToAddServer()
+ else -> {
+ navigateToAddFolder()
+ }
}
}
private fun importSharedMedia(imageIntent: Intent?) {
if (imageIntent?.action != Intent.ACTION_SEND) return
-
- val uri = imageIntent.data ?: if ((imageIntent.clipData?.itemCount
- ?: 0) > 0
- ) imageIntent.clipData?.getItemAt(0)?.uri else null
+ val uri =
+ imageIntent.data ?: imageIntent.clipData?.takeIf { it.itemCount > 0 }?.getItemAt(0)?.uri
val path = uri?.path ?: return
-
if (path.contains(packageName)) return
mSnackBar?.show()
-
lifecycleScope.launch(Dispatchers.IO) {
val media = Picker.import(this@MainActivity, getSelectedProject(), uri)
-
lifecycleScope.launch(Dispatchers.Main) {
mSnackBar?.dismiss()
intent = null
-
if (media != null) {
- preview()
+ navigateToPreview()
}
}
}
}
- private fun preview() {
+ private fun navigateToPreview() {
val projectId = getSelectedProject()?.id ?: return
-
PreviewActivity.start(this, projectId)
}
+ // ----- Permissions & Intent Handling -----
+ private fun checkNotificationPermissions() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ when {
+ ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED -> Timber.d("We have notifications permissions")
+
+ shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> showNotificationPermissionRationale()
+ else -> requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ }
+ }
+
+ private fun showNotificationPermissionRationale() {
+ Utility.showMaterialWarning(this, "Accept!") { Timber.d("thing") }
+ }
+
+ private fun handleIntent(intent: Intent) {
+ if (intent.action == Intent.ACTION_VIEW) {
+ intent.data?.takeIf { it.scheme == "save-veilid" }?.let { processUri(it) }
+ }
+ }
+
+ private fun processUri(uri: Uri) {
+ val path = uri.path
+ val queryParams = uri.queryParameterNames.associateWith { uri.getQueryParameter(it) }
+ AppLogger.d("Path: $path, QueryParams: $queryParams")
+ }
+
+ // ----- Overrides -----
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_main, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
+ val shouldShowSideMenu =
+ Space.current != null && mCurrentPagerItem != mPagerAdapter.settingsIndex
+ menu?.findItem(R.id.menu_folders)?.apply {
+ isVisible = shouldShowSideMenu
+ }
+ return super.onPrepareOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.menu_folders -> {
+ toggleDrawerState()
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
-
when (requestCode) {
2 -> Picker.pickMedia(this, mediaLaunchers.imagePickerLauncher)
REQUEST_CAMERA_PERMISSION -> {
@@ -430,118 +890,88 @@ class MainActivity : BaseActivity() {
}
}
+ // ----- Adapter Listeners -----
+ override fun onProjectSelected(project: Project) {
+ binding.root.closeDrawer(binding.drawerContent)
+ mCurrentPagerItem = mPagerAdapter.projects.indexOf(project)
+ }
- fun getSelectedProject(): Project? {
+ override fun getSelectedProject(): Project? {
return mPagerAdapter.getProject(mCurrentPagerItem)
}
- fun getSelectedSpace(): Space? {
- return Space.current
+ override fun onSpaceSelected(space: Space) {
+ Space.current = space
+ refreshSpace()
+ updateCurrentSpaceAtDrawer()
+ collapseSpacesList()
+ binding.root.closeDrawer(binding.drawerContent)
}
- private fun addClicked(mediaType: AddMediaType) {
-
- // Check if there's any project selected
- if (getSelectedProject() != null) {
-
- if (Prefs.addMediaHint) {
- when (mediaType) {
- AddMediaType.CAMERA -> {
-// takePhoto()
- Picker.takePhoto(this,mediaLaunchers.cameraLauncher)
- }
- AddMediaType.GALLERY -> Picker.pickMedia(
- this,
- mediaLaunchers.imagePickerLauncher
- )
-
- AddMediaType.FILES -> Picker.pickFiles(mediaLaunchers.filePickerLauncher)
- }
- } else {
-
- dialogManager.showInfoDialog(
- icon = R.drawable.perm_media_24px.asUiImage(),
- title = R.string.press_and_hold_options_media_screen_title.asUiText(),
- message = R.string.press_and_hold_options_media_screen_message.asUiText(),
- onDone = {
- Prefs.addMediaHint = true
- }
- )
- }
-
-
- } else if (Space.current == null) { // Check if there's any space available
- startActivity(Intent(this, SpaceSetupActivity::class.java))
- } else {
-
- if (!Prefs.addFolderHintShown) {
- AlertHelper.show(
- this,
- R.string.before_adding_media_create_a_new_folder_first,
- R.string.to_get_started_please_create_a_folder,
- R.drawable.ic_folder,
- buttons = listOf(
- AlertHelper.positiveButton(R.string.add_a_folder) { _, _ ->
- Prefs.addFolderHintShown = true
-
- addFolder()
- },
- AlertHelper.negativeButton(R.string.lbl_Cancel)
- )
- )
- } else {
- addFolder()
- }
- }
+ override fun onAddNewSpace() {
+ collapseSpacesList()
+ closeDrawer()
+ val intent = Intent(this, SpaceSetupActivity::class.java)
+ startActivity(intent)
}
- private fun checkNotificationPermissions() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- when {
- ContextCompat.checkSelfPermission(
- this,
- Manifest.permission.POST_NOTIFICATIONS
- ) == PackageManager.PERMISSION_GRANTED -> {
- Timber.d("We have notifications permissions")
- }
+ override fun getSelectedSpace(): Space? {
+ val currentSpace = Space.current
+ AppLogger.i("current space requested by adapter... = $currentSpace")
+ return Space.current
+ }
- shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> {
- showNotificationPermissionRationale()
- }
- else -> {
- requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
- }
- }
+ fun updateAfterDelete(done: Boolean) {
+ mMenuDelete?.isVisible = !done
+ if (done) {
+ refreshCurrentFolderCount()
}
}
- private fun showNotificationPermissionRationale() {
- Utility.showMaterialWarning(this, "Accept!") {
- Timber.d("thing")
+ /**
+ * Show the UploadManagerFragment as a Bottom Sheet.
+ * Ensures we only show one instance.
+ */
+ fun showUploadManagerFragment() {
+ if (uploadManagerFragment == null) {
+ uploadManagerFragment = UploadManagerFragment()
+ uploadManagerFragment?.show(supportFragmentManager, UploadManagerFragment.TAG)
+
+ // Stop the upload service when the bottom sheet is shown
+ UploadService.stopUploadService(this)
}
}
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
-
- menuInflater.inflate(R.menu.menu_main, menu)
-
- return super.onCreateOptionsMenu(menu)
- }
-
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
-// R.id.snowbird_menu -> {
-// val intent = Intent(this, SpaceSetupActivity::class.java)
-// intent.putExtra("snowbird", true)
-// startActivity(intent)
-// true
-// }
- else -> super.onOptionsItemSelected(item)
+ /**
+ * Setup a listener to detect when the UploadManagerFragment is dismissed.
+ * If there are pending uploads, restart the UploadService.
+ */
+ private fun setupBottomSheetObserver() {
+ supportFragmentManager.addFragmentOnAttachListener { _, fragment ->
+ if (fragment is UploadManagerFragment) {
+ uploadManagerFragment = fragment
+
+ // Observe when it gets dismissed
+ fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ uploadManagerFragment = null // Clear reference
+
+ // Check if there are pending uploads
+ if (Media.getByStatus(
+ listOf(Media.Status.Queued, Media.Status.Uploading),
+ Media.ORDER_PRIORITY
+ ).isNotEmpty()
+ ) {
+ UploadService.startUploadService(this@MainActivity)
+ }
+ }
+ })
+ }
}
}
+
private fun takePhoto() {
// Check if CAMERA permission is granted
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
@@ -565,5 +995,4 @@ class MainActivity : BaseActivity() {
private const val REQUEST_CAMERA_PERMISSION = 100
private const val REQUEST_IMAGE_CAPTURE = 101
}
-
}
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 b64b3647..06c4b4c5 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
@@ -7,7 +7,6 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
-import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
@@ -22,18 +21,24 @@ import net.opendasharchive.openarchive.databinding.FragmentMainMediaBinding
import net.opendasharchive.openarchive.databinding.ViewSectionBinding
import net.opendasharchive.openarchive.db.Collection
import net.opendasharchive.openarchive.db.Media
-import net.opendasharchive.openarchive.db.MediaAdapter
-import net.opendasharchive.openarchive.db.MediaViewHolder
import net.opendasharchive.openarchive.db.Space
+import net.opendasharchive.openarchive.features.core.BaseFragment
+import net.opendasharchive.openarchive.features.core.UiText
+import net.opendasharchive.openarchive.features.core.dialog.DialogType
+import net.opendasharchive.openarchive.features.core.dialog.showDialog
+import net.opendasharchive.openarchive.features.main.adapters.MainMediaAdapter
import net.opendasharchive.openarchive.upload.BroadcastManager
+import net.opendasharchive.openarchive.upload.UploadService
import net.opendasharchive.openarchive.util.AlertHelper
+import net.opendasharchive.openarchive.util.extensions.Position
import net.opendasharchive.openarchive.util.extensions.toggle
+import org.koin.androidx.viewmodel.ext.android.activityViewModel
import kotlin.collections.set
-class MainMediaFragment : Fragment() {
+class MainMediaFragment : BaseFragment() {
companion object {
- private const val COLUMN_COUNT = 4
+ private const val COLUMN_COUNT = 3
private const val ARG_PROJECT_ID = "project_id"
fun newInstance(projectId: Long): MainMediaFragment {
@@ -47,11 +52,16 @@ class MainMediaFragment : Fragment() {
}
}
- private var mAdapters = HashMap()
+ private val viewModel by activityViewModel()
+
+ private var mAdapters = HashMap()
private var mSection = HashMap()
private var mProjectId = -1L
private var mCollections = mutableMapOf()
+ private var selectedMediaIds = mutableSetOf()
+ private var isSelecting = false
+
private lateinit var binding: FragmentMainMediaBinding
private val mMessageReceiver: BroadcastReceiver = object : BroadcastReceiver() {
@@ -80,11 +90,6 @@ class MainMediaFragment : Fragment() {
}
}
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setHasOptionsMenu(true)
- }
-
override fun onStart() {
super.onStart()
BroadcastManager.register(requireContext(), mMessageReceiver)
@@ -95,23 +100,9 @@ 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 -> {
- AlertHelper.show(
- requireContext(), R.string.confirm_remove_media, null, buttons = listOf(
- AlertHelper.positiveButton(R.string.remove) { _, _ ->
- deleteSelected()
- },
- AlertHelper.negativeButton()
- )
- )
- true
- }
-
- else -> super.onOptionsItemSelected(item)
- }
+ override fun onPause() {
+ cancelSelection()
+ super.onPause()
}
override fun onCreateView(
@@ -128,30 +119,35 @@ class MainMediaFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ viewModel.log("MainMediaFragment onCreateView called for project Id $mProjectId")
- if (mProjectId == -1L) {
- val space = Space.current
- val text: String = if (space != null) {
- val projects = space.projects
- if (projects.isNotEmpty()) {
- getString(R.string.tap_to_add)
- } else {
- "Tap the button below to add media folder."
- }
+ val space = Space.current
+ val text: String = if (space != null) {
+ val projects = space.projects
+ if (projects.isNotEmpty()) {
+ getString(R.string.tap_to_add)
} else {
- "Tap the button below to add media server."
+ "Tap the button below to add media folder"
}
+ } else {
+ "Tap the button below to add media server"
+ }
- binding.tvWelcomeDescr.text = text
+ binding.tvWelcomeDescr.text = text
+
+ if (space != null) {
+ binding.tvWelcome.visibility = View.INVISIBLE
+ } else {
+ binding.tvWelcome.visibility = View.VISIBLE
}
+
refresh()
}
fun updateProjectItem(collectionId: Long, mediaId: Long, progress: Int, isUploaded: Boolean) {
AppLogger.i("Current progress for $collectionId: ", progress)
mAdapters[collectionId]?.apply {
-
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
updateItem(mediaId, progress, isUploaded)
if (progress == -1) {
@@ -209,6 +205,13 @@ class MainMediaFragment : Fragment() {
binding.addMediaHint.toggle(mCollections.isEmpty())
}
+ fun cancelSelection() {
+ isSelecting = false
+ selectedMediaIds.clear()
+ mAdapters.values.forEach { it.clearSelections() }
+ updateSelectionCount()
+ }
+
fun deleteSelected() {
val toDelete = ArrayList()
@@ -229,20 +232,26 @@ class MainMediaFragment : Fragment() {
private fun createMediaList(collection: Collection, media: List): View {
val holder = SectionViewHolder(ViewSectionBinding.inflate(layoutInflater))
-
holder.recyclerView.setHasFixedSize(true)
holder.recyclerView.layoutManager = GridLayoutManager(activity, COLUMN_COUNT)
holder.setHeader(collection, media)
- val mediaAdapter = MediaAdapter(
- requireActivity(),
- { MediaViewHolder.Box(it) },
- media,
- holder.recyclerView
- ) {
- (activity as? MainActivity)?.updateAfterDelete(mAdapters.values.firstOrNull { it.selecting } == null)
- }
+ val mediaAdapter = MainMediaAdapter(
+ activity = requireActivity(),
+ mediaList = media,
+ recyclerView = holder.recyclerView,
+ checkSelecting = { updateSelectionState() },
+ onDeleteClick = { mediaItem, itemPosition ->
+ showDeleteConfirmationDialog(
+ mediaItem = mediaItem,
+ onDeleteItem = {
+ onDeleteItem(collectionId = collection.id, itemPosition = itemPosition)
+ }
+ )
+
+ }
+ )
holder.recyclerView.adapter = mediaAdapter
mAdapters[collection.id] = mediaAdapter
@@ -251,6 +260,70 @@ class MainMediaFragment : Fragment() {
return holder.root
}
+ private fun onDeleteItem(collectionId: Long, itemPosition: Int) {
+ val adapter = mAdapters[collectionId]
+ adapter?.deleteItem(itemPosition)
+ }
+
+ private fun showDeleteConfirmationDialog(mediaItem: Media, onDeleteItem: () -> Unit) {
+
+ dialogManager.showDialog(dialogManager.requireResourceProvider()) {
+ type = DialogType.Error
+ title = UiText.StringResource(R.string.upload_unsuccessful)
+ message = UiText.StringResource(R.string.upload_unsuccessful_description)
+ positiveButton {
+ text = UiText.StringResource(R.string.retry)
+ action = {
+ mediaItem.apply {
+ sStatus = Media.Status.Queued
+ statusMessage = ""
+ save()
+ BroadcastManager.postChange(
+ requireActivity(),
+ mediaItem.collectionId,
+ mediaItem.id
+ )
+ }
+ UploadService.startUploadService(requireActivity())
+ }
+ }
+ destructiveButton {
+ text = UiText.StringResource(R.string.btn_lbl_remove_media)
+ action = {
+ onDeleteItem.invoke()
+ }
+ }
+ }
+// AlertHelper.show(
+// context = requireContext(),
+// message = getString(R.string.upload_unsuccessful_description),
+// title = R.string.upload_unsuccessful,
+// icon = R.drawable.ic_error,
+// buttons = listOf(
+// AlertHelper.positiveButton(R.string.retry) { _, _ ->
+//
+// },
+// AlertHelper.negativeButton(R.string.remove) { _, _ ->
+// onDeleteItem.invoke()
+// },
+// AlertHelper.neutralButton()
+// )
+// )
+ }
+
+ //update selection UI by summing selected counts from all adapters.
+ fun updateSelectionState() {
+ val isSelecting = mAdapters.values.any { it.selecting }
+ (activity as? MainActivity)?.setSelectionMode(isSelecting)
+ val totalSelected = mAdapters.values.sumOf { it.getSelectedCount() }
+ (activity as? MainActivity)?.updateSelectedCount(totalSelected)
+ }
+
+
+ private fun updateSelectionCount() {
+ (activity as? MainActivity)?.updateSelectedCount(selectedMediaIds.size)
+ }
+
private fun deleteCollections(collectionIds: List, cleanup: Boolean) {
collectionIds.forEach { collectionId ->
mAdapters.remove(collectionId)
@@ -266,4 +339,10 @@ class MainMediaFragment : Fragment() {
}
}
}
+
+ fun showUploadManager() {
+ (activity as? MainActivity)?.showUploadManagerFragment()
+ }
+
+ override fun getToolbarTitle(): String = ""
}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainViewModel.kt
new file mode 100644
index 00000000..fba870c1
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainViewModel.kt
@@ -0,0 +1,38 @@
+package net.opendasharchive.openarchive.features.main
+
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import net.opendasharchive.openarchive.core.logger.AppLogger
+
+class MainViewModel : ViewModel() {
+
+ private val _uiState = MutableStateFlow(
+ MainUiState(
+ currentPagerItem = 0
+ )
+ )
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ init {
+
+ AppLogger.i("MainViewModel initialized....")
+ }
+
+
+ fun log(msg: String) {
+ AppLogger.i("MainViewModel: $msg")
+ }
+
+ fun updateCurrentPagerItem(page: Int) {
+ _uiState.update { it.copy(currentPagerItem = page) }
+ }
+
+ fun getCurrentPagerItem(): Int = _uiState.value.currentPagerItem
+}
+
+data class MainUiState(
+ val currentPagerItem: Int
+)
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt
index 1cf0d1b5..60f34bff 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt
@@ -6,6 +6,9 @@ import net.opendasharchive.openarchive.db.Collection
import net.opendasharchive.openarchive.db.Media
import java.text.DateFormat
import java.text.NumberFormat
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
data class SectionViewHolder(
private val binding: ViewSectionBinding
@@ -13,11 +16,16 @@ data class SectionViewHolder(
companion object {
- private val mNf
- get() = NumberFormat.getIntegerInstance()
+ private val mNf = NumberFormat.getIntegerInstance()
- private val mDf
- get() = DateFormat.getDateTimeInstance()
+ private val mDf = DateFormat.getDateTimeInstance()
+
+ private val dateFormat = SimpleDateFormat("MMM dd, yyyy | h:mma", Locale.ENGLISH)
+
+ fun formatWithLowercaseAmPm(date: Date): String {
+ val formatted = dateFormat.format(date)
+ return formatted.replace("AM", "am").replace("PM", "pm")
+ }
}
@@ -33,24 +41,15 @@ data class SectionViewHolder(
val recyclerView
get() = binding.recyclerView
- fun setHeader(
- collection: Collection,
- media: List
- ) {
+ fun setHeader(collection: Collection, media: List) {
if (media.any { it.isUploading }) {
timestamp.setText(R.string.uploading)
-
val uploaded = media.filter { it.sStatus == Media.Status.Uploaded }.size
-
count.text = count.context.getString(R.string.counter, uploaded, media.size)
-
return
}
-
count.text = mNf.format(media.size)
-
val uploadDate = collection.uploadDate
-
- timestamp.text = if (uploadDate != null) mDf.format(uploadDate) else "Ready to upload"
+ timestamp.text = if (uploadDate != null) formatWithLowercaseAmPm(uploadDate) else "Ready to upload"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/FolderDrawerAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/FolderDrawerAdapter.kt
new file mode 100644
index 00000000..6e068676
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/FolderDrawerAdapter.kt
@@ -0,0 +1,104 @@
+package net.opendasharchive.openarchive.features.main.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.databinding.RvDrawerRowBinding
+import net.opendasharchive.openarchive.db.Project
+
+
+interface FolderDrawerAdapterListener {
+ fun onProjectSelected(project: Project)
+ fun getSelectedProject(): Project?
+}
+
+class FolderDrawerAdapter(
+ private val listener: FolderDrawerAdapterListener
+) : ListAdapter(DIFF_CALLBACK) {
+
+ private var selectedProject: Project? = listener.getSelectedProject()
+
+ inner class FolderViewHolder(
+ private val binding: RvDrawerRowBinding,
+ private val listener: FolderDrawerAdapterListener
+ ) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(project: Project) {
+
+ binding.rvTitle.text = project.description
+
+ val isSelected = project.id == selectedProject?.id
+ val iconRes = if (isSelected) R.drawable.baseline_folder_white_24 else R.drawable.outline_folder_white_24
+ val iconColor = if (isSelected) R.color.colorTertiary else R.color.colorOnBackground
+ val textColor = if (isSelected) R.color.colorOnBackground else R.color.colorText
+
+ val icon = ContextCompat.getDrawable(binding.rvIcon.context, iconRes)
+ icon?.setTint(ContextCompat.getColor(binding.rvIcon.context, iconColor))
+ binding.rvIcon.setImageDrawable(icon)
+
+ binding.rvTitle.setTextColor(ContextCompat.getColor(binding.rvTitle.context, textColor))
+
+ binding.root.setOnClickListener {
+ onItemSelected(project)
+ }
+ }
+
+ private fun onItemSelected(project: Project) {
+ val previousIndex = currentList.indexOf(selectedProject)
+ val newIndex = currentList.indexOf(project)
+
+ selectedProject = project
+
+ if (previousIndex != -1) notifyItemChanged(previousIndex)
+ if (newIndex != -1) notifyItemChanged(newIndex)
+
+ listener.onProjectSelected(project)
+ }
+ }
+
+ companion object {
+ private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: Project, newItem: Project): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: Project, newItem: Project): Boolean {
+ return oldItem.description == newItem.description
+ }
+ }
+ }
+
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
+ val binding = RvDrawerRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return FolderViewHolder(binding, listener = listener)
+ }
+
+ override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
+ val project = getItem(position)
+
+ holder.bind(project)
+ }
+
+ fun update(projects: List) {
+ // Preserve selection if the selected project is still present
+ val previouslySelectedId = selectedProject?.id
+ selectedProject = projects.find { it.id == previouslySelectedId }
+
+ submitList(projects)
+ }
+
+ fun updateSelectedProject(project: Project?) {
+ val previousIndex = currentList.indexOf(selectedProject)
+ val newIndex = currentList.indexOf(project)
+
+ selectedProject = project
+
+ if (previousIndex != -1) notifyItemChanged(previousIndex)
+ if (newIndex != -1) notifyItemChanged(newIndex)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/MainMediaAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/MainMediaAdapter.kt
new file mode 100644
index 00000000..56089450
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/MainMediaAdapter.kt
@@ -0,0 +1,320 @@
+package net.opendasharchive.openarchive.features.main.adapters
+
+import android.app.Activity
+import android.content.Intent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.snackbar.Snackbar
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.logger.AppLogger
+import net.opendasharchive.openarchive.databinding.RvMediaBoxBinding
+import net.opendasharchive.openarchive.db.Media
+import net.opendasharchive.openarchive.features.main.MainActivity
+import net.opendasharchive.openarchive.features.media.PreviewActivity
+import net.opendasharchive.openarchive.upload.BroadcastManager
+import net.opendasharchive.openarchive.upload.UploadManagerActivity
+import net.opendasharchive.openarchive.upload.UploadService
+import net.opendasharchive.openarchive.util.AlertHelper
+import java.lang.ref.WeakReference
+
+class MainMediaAdapter(
+ private val activity: Activity?,
+ private val mediaList: List,
+ private val recyclerView: RecyclerView,
+ private val checkSelecting: () -> Unit,
+ private val allowMultiProjectSelection: Boolean = false,
+ private val onDeleteClick: (Media, Int) -> Unit,
+) : RecyclerView.Adapter() {
+
+ companion object {
+ private const val PAYLOAD_SELECTION = "selection"
+ private const val PAYLOAD_PROGRESS = "progress"
+
+ private val supportedStatuses: List = listOf(
+ Media.Status.Local, Media.Status.Uploading, Media.Status.Error
+ )
+ }
+
+ var media: ArrayList = ArrayList(mediaList)
+ private set
+
+ var doImageFade = true
+
+ var isEditMode = false
+
+ var selecting = false
+
+ private var mActivity = WeakReference(activity)
+
+ private val selectedItems = mutableSetOf()
+
+ init {
+ setHasStableIds(true)
+ }
+
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainMediaViewHolder {
+ val binding = RvMediaBoxBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ val mvh = MainMediaViewHolder(binding)
+
+ // Normal click: either toggle selection if already in selection mode or perform normal action.
+ mvh.itemView.setOnClickListener { v ->
+ val pos = recyclerView.getChildLayoutPosition(v)
+ if (pos == RecyclerView.NO_POSITION) return@setOnClickListener
+ if (selecting) {
+ toggleSelection(pos)
+ } else {
+ handleNormalClick(pos)
+ }
+ }
+
+ // Long-click: enable selection mode (if not already enabled) and toggle selection.
+ mvh.itemView.setOnLongClickListener { v ->
+ val pos = recyclerView.getChildLayoutPosition(v)
+ if (pos == RecyclerView.NO_POSITION) return@setOnLongClickListener true
+ if (!selecting) {
+ selecting = true
+ // If multi-project selection is allowed, the parent fragment may already have enabled selection
+ // on other adapters. Otherwise, we are only enabling it here.
+ checkSelecting.invoke()
+ }
+ toggleSelection(pos)
+ true
+ }
+
+ return mvh
+ }
+
+ override fun getItemCount(): Int = media.size
+
+ override fun getItemId(position: Int): Long = media[position].id
+
+ override fun onBindViewHolder(holder: MainMediaViewHolder, position: Int) {
+ AppLogger.i("onBindViewHolder called for position $position")
+ holder.bind(media[position], selecting, doImageFade)
+ }
+
+ override fun onBindViewHolder(
+ holder: MainMediaViewHolder, position: Int, payloads: MutableList
+ ) {
+ if (payloads.isNotEmpty()) {
+ val payload = payloads[0]
+ when (payload) {
+ "progress" -> {
+ holder.updateProgress(media[position].uploadPercentage ?: 0)
+ }
+
+ "full" -> {
+ holder.bind(media[position], selecting, doImageFade)
+ }
+ }
+ } else {
+ holder.bind(media[position], selecting, doImageFade)
+ }
+ }
+
+ // --- Helper functions for selection handling ---
+ private fun toggleSelection(position: Int) {
+ val item = media[position]
+ item.selected = !item.selected
+ item.save()
+ notifyItemChanged(position)
+ // Update the adapter’s overall selecting flag.
+ selecting = media.any { it.selected }
+ checkSelecting.invoke()
+ }
+
+ private fun handleNormalClick(position: Int) {
+ val item = media[position]
+ val mediaStatus = item.sStatus
+ // Default behavior if needed.
+ if (mediaStatus == Media.Status.Local) {
+ if (supportedStatuses.contains(Media.Status.Local)) {
+ mActivity.get()?.let {
+ PreviewActivity.start(it, item.projectId)
+ }
+ }
+ } else if (mediaStatus == Media.Status.Queued || mediaStatus == Media.Status.Uploading) {
+ if (supportedStatuses.contains(Media.Status.Uploading)) {
+ (mActivity.get() as? MainActivity)?.showUploadManagerFragment()
+ }
+ } else if (mediaStatus == Media.Status.Error) {
+ if (supportedStatuses.contains(Media.Status.Error)) {
+ onDeleteClick.invoke(item, position)
+ }
+ }
+ }
+
+ fun updateItem(mediaId: Long, progress: Int, isUploaded: Boolean = false): Boolean {
+ val idx = media.indexOfFirst { it.id == mediaId }
+ AppLogger.i("updateItem: mediaId=$mediaId idx=$idx")
+ if (idx < 0) return false
+
+ val item = media[idx]
+
+ if (isUploaded) {
+ item.status = Media.Status.Uploaded.id
+ AppLogger.i("Media item $mediaId uploaded, notifying item changed at position $idx")
+ notifyItemChanged(idx, "full")
+ } else if (progress >= 0) {
+ item.uploadPercentage = progress
+ item.status = Media.Status.Uploading.id
+ notifyItemChanged(idx, "progress")
+ }
+
+ return true
+ }
+
+ fun removeItem(mediaId: Long): Boolean {
+ val idx = media.indexOfFirst { it.id == mediaId }
+ if (idx < 0) return false
+ media.removeAt(idx)
+ notifyItemRemoved(idx)
+ checkSelecting.invoke()
+ return true
+ }
+
+ fun updateData(newMediaList: List) {
+ val diffCallback = MediaDiffCallback(this.media, newMediaList)
+ val diffResult = DiffUtil.calculateDiff(diffCallback)
+ media.clear()
+ media.addAll(newMediaList)
+ diffResult.dispatchUpdatesTo(this)
+ }
+
+ fun clearSelections() {
+ selectedItems.clear()
+ media.forEach { it.selected = false }
+ notifyDataSetChanged()
+ }
+
+ private fun selectView(view: View) {
+ if (!selecting) return
+
+ val mediaId = view.tag as? Long ?: return
+ val wasSelected = selectedItems.contains(mediaId)
+
+ if (wasSelected) {
+ selectedItems.remove(mediaId)
+ } else {
+ if (!allowMultiProjectSelection) {
+ selectedItems.clear()
+ media.forEach { it.selected = false }
+ }
+ selectedItems.add(mediaId)
+ }
+
+ media.firstOrNull { it.id == mediaId }?.selected = !wasSelected
+ checkSelecting.invoke()
+ notifyItemChanged(media.indexOfFirst { it.id == mediaId })
+ }
+
+ fun onItemMove(oldPos: Int, newPos: Int) {
+ if (!isEditMode) return
+
+ val mediaToMov = media.removeAt(oldPos)
+ media.add(newPos, mediaToMov)
+
+ var priority = media.size
+
+ for (item in media) {
+ item.priority = priority--
+ item.save()
+ }
+
+ notifyItemMoved(oldPos, newPos)
+ }
+
+ fun deleteItem(pos: Int) {
+ if (pos < 0 || pos >= media.size) return
+
+ val item = media[pos]
+ var undone = false
+
+ val snackbar =
+ Snackbar.make(recyclerView, R.string.confirm_remove_media, Snackbar.LENGTH_LONG)
+ snackbar.setAction(R.string.undo) { _ ->
+ undone = true
+ media.add(pos, item)
+
+ notifyItemInserted(pos)
+ }
+
+ snackbar.addCallback(object : Snackbar.Callback() {
+ override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
+ if (!undone) {
+ val collection = item.collection
+
+ // Delete collection along with the item, if the collection
+ // would become empty.
+ if ((collection?.size ?: 0) < 2) {
+ collection?.delete()
+ } else {
+ item.delete()
+ }
+
+ BroadcastManager.postDelete(recyclerView.context, item.id)
+ }
+
+ super.onDismissed(transientBottomBar, event)
+ }
+ })
+
+ snackbar.show()
+
+ removeItem(item.id)
+
+ mActivity.get()?.let {
+ BroadcastManager.postDelete(it, item.id)
+ }
+ }
+
+ fun getSelectedCount(): Int = media.count { it.selected }
+
+ fun deleteSelected(): Boolean {
+ var hasDeleted = false
+ // Copy list to avoid concurrent modification.
+ val selectedItems = media.filter { it.selected }
+ selectedItems.forEach { item ->
+ val idx = media.indexOf(item)
+ if (idx != -1) {
+ media.removeAt(idx)
+ notifyItemRemoved(idx)
+ item.delete()
+ hasDeleted = true
+ }
+ }
+ selecting = false
+ checkSelecting.invoke()
+ return hasDeleted
+ }
+}
+
+private class MediaDiffCallback(
+ private val oldList: List, private val newList: List
+) : DiffUtil.Callback() {
+
+ override fun getOldListSize() = oldList.size
+
+ override fun getNewListSize() = newList.size
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ return oldList[oldItemPosition].id == newList[newItemPosition].id
+ }
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ // Compare only the fields that affect the UI
+
+ val oldItem = oldList[oldItemPosition]
+ val newItem = newList[newItemPosition]
+
+ return oldItem.status == newItem.status && oldItem.uploadPercentage == newItem.uploadPercentage && oldItem.selected == newItem.selected && oldItem.title == newItem.title
+ }
+
+ override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
+ return super.getChangePayload(oldItemPosition, newItemPosition)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/MainMediaViewHolder.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/MainMediaViewHolder.kt
new file mode 100644
index 00000000..51440f9c
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/MainMediaViewHolder.kt
@@ -0,0 +1,202 @@
+package net.opendasharchive.openarchive.features.main.adapters
+
+import android.widget.ImageView
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import androidx.swiperefreshlayout.widget.CircularProgressDrawable
+import coil3.ImageLoader
+import coil3.load
+import coil3.request.crossfade
+import coil3.request.placeholder
+import coil3.video.VideoFrameDecoder
+import coil3.video.videoFrameMillis
+import com.github.derlio.waveform.soundfile.SoundFile
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.logger.AppLogger
+import net.opendasharchive.openarchive.databinding.RvMediaBoxBinding
+import net.opendasharchive.openarchive.db.Media
+import net.opendasharchive.openarchive.util.extensions.hide
+import net.opendasharchive.openarchive.util.extensions.show
+import timber.log.Timber
+
+class MainMediaViewHolder(val binding: RvMediaBoxBinding) : RecyclerView.ViewHolder(binding.root) {
+
+ companion object {
+ val soundCache = HashMap()
+ }
+
+ private val mContext = itemView.context
+
+ private val imageLoader = ImageLoader.Builder(mContext)
+ .components {
+ add(VideoFrameDecoder.Factory())
+ }
+ .build()
+
+
+ fun bind(media: Media? = null, isInSelectionMode: Boolean = false, doImageFade: Boolean = true) {
+
+ itemView.tag = media?.id
+
+ // Update selection visuals.
+ if (isInSelectionMode && media?.selected == true) {
+ itemView.setBackgroundResource(R.color.colorTertiary)
+ binding.selectedIndicator.show()
+ } else {
+ itemView.setBackgroundResource(R.color.transparent)
+ binding.selectedIndicator.hide()
+ }
+
+ binding.image.alpha = if (media?.sStatus == Media.Status.Uploaded || !doImageFade) 1f else 0.5f
+
+ if (media?.mimeType?.startsWith("image") == true) {
+ val progress = CircularProgressDrawable(mContext)
+ progress.strokeWidth = 5f
+ progress.centerRadius = 30f
+ progress.start()
+
+ binding.image.load(media.fileUri, imageLoader) {
+ placeholder(progress)
+ crossfade(true)
+ crossfade(300)
+ listener(onError = { req, res ->
+ AppLogger.e(res.throwable)
+ })
+ }
+
+ binding.image.scaleType = ImageView.ScaleType.CENTER_CROP
+ binding.image.show()
+ binding.waveform.hide()
+ binding.videoIndicator.hide()
+ } else if (media?.mimeType?.startsWith("video") == true) {
+
+ binding.image.load(media.originalFilePath, imageLoader) {
+ val progress = CircularProgressDrawable(mContext)
+ progress.strokeWidth = 5f
+ progress.centerRadius = 30f
+ progress.start()
+ videoFrameMillis(1000) // Extracts the frame at 1 second (1000ms)
+ placeholder(progress)
+ crossfade(true)
+ crossfade(300)
+ listener(onError = { req, res -> AppLogger.e(res.throwable) })
+ }
+
+ binding.image.scaleType = ImageView.ScaleType.CENTER_CROP
+ binding.image.show()
+ binding.waveform.hide()
+ binding.videoIndicator.show()
+ } else if (media?.mimeType?.startsWith("audio") == true) {
+ binding.videoIndicator.hide()
+
+ val soundFile = soundCache[media.originalFilePath]
+
+ if (soundFile != null) {
+ binding.image.hide()
+ binding.waveform.setAudioFile(soundFile)
+ binding.waveform.show()
+ } else {
+ binding.image.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.no_thumbnail))
+ binding.image.scaleType = ImageView.ScaleType.CENTER_CROP
+ binding.image.show()
+ binding.waveform.hide()
+
+ CoroutineScope(Dispatchers.IO).launch {
+ @Suppress("NAME_SHADOWING")
+ val soundFile = try {
+ SoundFile.create(media.originalFilePath) {
+ return@create true
+ }
+ } catch (e: Throwable) {
+ Timber.d(e)
+
+ null
+ }
+
+ if (soundFile != null) {
+ soundCache[media.originalFilePath] = soundFile
+
+ MainScope().launch {
+ binding.waveform.setAudioFile(soundFile)
+ binding.image.hide()
+ binding.waveform.show()
+ }
+ }
+ }
+ }
+ } else if (media?.mimeType?.startsWith("application") == true) {
+ binding.image.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.ic_unknown_file))
+ binding.image.scaleType = ImageView.ScaleType.CENTER_INSIDE
+ binding.image.show()
+ binding.waveform.hide()
+ binding.videoIndicator.hide()
+ } else {
+ binding.image.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.ic_unknown_file))
+ binding.image.scaleType = ImageView.ScaleType.CENTER_INSIDE
+ binding.image.show()
+ binding.waveform.hide()
+ binding.videoIndicator.hide()
+ }
+
+ // Update overlay based on media status.
+ when (media?.sStatus) {
+ Media.Status.Error -> {
+ AppLogger.i("Media Item ${media.id} is error")
+
+ binding.overlayContainer.show()
+ binding.progress.hide()
+ binding.progressText.hide()
+ binding.error.show()
+
+ }
+ Media.Status.Queued -> {
+ AppLogger.i("Media Item ${media.id} is queued")
+ binding.overlayContainer.show()
+ binding.progress.isIndeterminate = true
+ binding.progress.show()
+ binding.progressText.hide()
+ binding.error.hide()
+ }
+ Media.Status.Uploading -> {
+ binding.progress.isIndeterminate = false
+ val progressValue = media.uploadPercentage ?: 0
+ AppLogger.i("Media Item ${media.id} is uploading")
+
+ binding.overlayContainer.show()
+ binding.progress.show()
+ binding.progressText.show()
+
+ // Make sure to keep spinning until the upload has made some noteworthy progress.
+ if (progressValue > 2) {
+ binding.progress.setProgressCompat(progressValue, true)
+ }
+ binding.progressText.text = "${progressValue}%"
+ binding.error.hide()
+ }
+ else -> {
+ binding.overlayContainer.hide()
+ binding.progress.hide()
+ binding.progressText.hide()
+ binding.error.hide()
+ }
+ }
+
+ }
+
+ fun updateProgress(progressValue: Int) {
+ if (progressValue > 2) {
+ binding.progress.isIndeterminate = false
+ binding.progress.setProgressCompat(progressValue, true)
+ } else {
+ binding.progress.isIndeterminate = true
+ }
+
+ AppLogger.i("Updating progressText to $progressValue%")
+ binding.progressText.show(animate = true)
+ binding.progressText.text = "$progressValue%"
+ }
+}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/SpaceDrawerAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/SpaceDrawerAdapter.kt
new file mode 100644
index 00000000..af34f046
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/SpaceDrawerAdapter.kt
@@ -0,0 +1,146 @@
+package net.opendasharchive.openarchive.features.main.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.databinding.RvDrawerRowBinding
+import net.opendasharchive.openarchive.db.Space
+import net.opendasharchive.openarchive.util.extensions.scaled
+
+interface SpaceDrawerAdapterListener {
+ fun onSpaceSelected(space: Space)
+ fun onAddNewSpace()
+ fun getSelectedSpace(): Space?
+}
+
+class SpaceDrawerAdapter(private val listener: SpaceDrawerAdapterListener) : ListAdapter(DIFF_CALLBACK) {
+
+ private var selectedSpace: Space? = listener.getSelectedSpace()
+
+ sealed class SpaceItem {
+ data class SpaceItemData(val space: Space) : SpaceItem()
+ data object AddSpaceItem : SpaceItem()
+ }
+
+ companion object {
+
+ private const val VIEW_TYPE_SPACE = 0
+ private const val VIEW_TYPE_ADD = 1
+
+ private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: SpaceItem, newItem: SpaceItem): Boolean {
+ return when {
+ oldItem is SpaceItem.SpaceItemData && newItem is SpaceItem.SpaceItemData -> oldItem.space.id == newItem.space.id
+ oldItem is SpaceItem.AddSpaceItem && newItem is SpaceItem.AddSpaceItem -> true
+ else -> false
+ }
+ }
+
+ override fun areContentsTheSame(oldItem: SpaceItem, newItem: SpaceItem): Boolean {
+ return when {
+ oldItem is SpaceItem.SpaceItemData && newItem is SpaceItem.SpaceItemData -> oldItem.space.friendlyName == newItem.space.friendlyName
+ oldItem is SpaceItem.AddSpaceItem && newItem is SpaceItem.AddSpaceItem -> true
+ else -> false
+ }
+ }
+ }
+ }
+
+ abstract class ItemTypeViewHolder(binding: RvDrawerRowBinding) : RecyclerView.ViewHolder(binding.root) {
+ abstract fun bind(item: SpaceItem)
+ }
+
+ inner class SpaceViewHolder(private val binding: RvDrawerRowBinding) : ItemTypeViewHolder(binding) {
+ override fun bind(item: SpaceItem) {
+
+ val space = (item as SpaceItem.SpaceItemData).space
+
+ val isSelected = listener.getSelectedSpace()?.id == space.id
+ val backgroundColor = if(isSelected) R.color.colorTertiary else R.color.c23_light_grey
+ val textColor = if (isSelected) R.color.colorOnBackground else R.color.colorText
+
+ binding.root.setBackgroundColor(binding.root.context.getColor(backgroundColor))
+
+ val icon = space.getAvatar(binding.rvIcon.context)?.scaled(21, binding.rvIcon.context)
+ icon?.setTint(binding.rvIcon.context.getColor(R.color.colorOnBackground))
+ binding.rvIcon.setImageDrawable(icon)
+
+ binding.rvTitle.text = space.friendlyName
+ binding.rvTitle.setTextColor(binding.rvTitle.context.getColor(textColor))
+
+ binding.root.setOnClickListener {
+ onItemSelected(space)
+ }
+ }
+
+ private fun onItemSelected(space: Space) {
+ val previousIndex = currentList.indexOfFirst { it is SpaceItem.SpaceItemData && it.space.id == selectedSpace?.id }
+ val newIndex = currentList.indexOfFirst { it is SpaceItem.SpaceItemData && it.space.id == space.id }
+
+ selectedSpace = space
+
+ if (previousIndex != -1) notifyItemChanged(previousIndex)
+ if (newIndex != -1) notifyItemChanged(newIndex)
+
+ listener.onSpaceSelected(space)
+ }
+ }
+
+ inner class AddSpaceViewHolder(private val binding: RvDrawerRowBinding) : ItemTypeViewHolder(binding) {
+ override fun bind(item: SpaceItem) {
+ val context = binding.rvTitle.context
+ binding.rvTitle.text = context.getString(R.string.add_another_account)
+ binding.rvTitle.setTextColor(ContextCompat.getColor(context, R.color.colorTertiary))
+
+ val icon = ContextCompat.getDrawable(context, R.drawable.ic_add)
+ icon?.setTint(ContextCompat.getColor(binding.rvIcon.context, R.color.colorTertiary))
+ binding.rvIcon.setImageDrawable(icon)
+
+ binding.root.setOnClickListener {
+ listener.onAddNewSpace()
+ }
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return when (getItem(position)) {
+ is SpaceItem.SpaceItemData -> VIEW_TYPE_SPACE
+ is SpaceItem.AddSpaceItem -> VIEW_TYPE_ADD
+ else -> throw IllegalArgumentException("Invalid view type")
+ }
+ }
+
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTypeViewHolder {
+ val binding = RvDrawerRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+
+ return when (viewType) {
+ VIEW_TYPE_SPACE -> SpaceViewHolder(binding)
+ VIEW_TYPE_ADD -> AddSpaceViewHolder(binding)
+ else -> throw IllegalArgumentException("Invalid view type")
+ }
+ }
+
+ override fun onBindViewHolder(holder: ItemTypeViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ fun update(spaces: List) {
+ val items = spaces.map { SpaceItem.SpaceItemData(it) } + SpaceItem.AddSpaceItem
+ submitList(items)
+ }
+
+ fun updateSelectedSpace(space: Space?) {
+ val previousIndex = currentList.indexOfFirst { it is SpaceItem.SpaceItemData && it.space.id == selectedSpace?.id }
+ val newIndex = currentList.indexOfFirst { it is SpaceItem.SpaceItemData && it.space.id == space?.id }
+
+ selectedSpace = space
+
+ if (previousIndex != -1) notifyItemChanged(previousIndex)
+ if (newIndex != -1) notifyItemChanged(newIndex)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/HomeScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/HomeScreen.kt
new file mode 100644
index 00000000..6e3a95fb
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/HomeScreen.kt
@@ -0,0 +1,382 @@
+package net.opendasharchive.openarchive.features.main.ui
+
+import android.content.Context
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+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.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material3.DrawerValue
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ModalNavigationDrawer
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.rememberDrawerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme
+import net.opendasharchive.openarchive.db.Project
+import net.opendasharchive.openarchive.db.Space
+import net.opendasharchive.openarchive.features.main.ui.components.HomeAppBar
+import net.opendasharchive.openarchive.features.main.ui.components.MainBottomBar
+import net.opendasharchive.openarchive.features.main.ui.components.MainDrawerContent
+import net.opendasharchive.openarchive.features.main.ui.components.SpaceIcon
+import net.opendasharchive.openarchive.features.media.AddMediaType
+import net.opendasharchive.openarchive.features.settings.SettingsScreen
+import org.koin.androidx.compose.koinViewModel
+import kotlin.math.max
+
+
+@Serializable
+data object HomeRoute
+
+@Serializable
+data object MediaCacheRoute
+
+@Composable
+fun SaveNavGraph(
+ context: Context,
+ viewModel: HomeViewModel = koinViewModel(),
+ onExit: () -> Unit,
+ onNewFolder: () -> Unit,
+ onFolderSelected: (Long) -> Unit,
+ onAddMedia: (AddMediaType) -> Unit
+) {
+ val navController = rememberNavController()
+
+ SaveAppTheme {
+
+ NavHost(
+ navController = navController,
+ startDestination = HomeRoute
+ ) {
+
+ composable {
+ HomeScreen(
+ viewModel = viewModel,
+ onExit = onExit,
+ onNewFolder = onNewFolder,
+ onFolderSelected = onFolderSelected,
+ onAddMedia = onAddMedia,
+ onNavigateToCache = {
+ navController.navigate(MediaCacheRoute)
+ }
+ )
+ }
+
+ composable {
+ MediaCacheScreen(context) {
+ navController.popBackStack()
+ }
+ }
+
+ }
+ }
+}
+
+@Composable
+fun HomeScreen(
+ viewModel: HomeViewModel = koinViewModel(),
+ onExit: () -> Unit,
+ onNewFolder: () -> Unit,
+ onFolderSelected: (Long) -> Unit,
+ onAddMedia: (AddMediaType) -> Unit,
+ onNavigateToCache: () -> Unit
+) {
+
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+
+ HomeScreenContent(
+ onExit = onExit,
+ state = state,
+ onAction = viewModel::onAction,
+ onNavigateToCache = onNavigateToCache
+ )
+
+
+}
+
+class HomeViewModel : ViewModel() {
+ private val _uiState = MutableStateFlow(HomeScreenState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ init {
+ loadSpacesAndFolders()
+ }
+
+ fun onAction(action: HomeScreenAction) {
+ when (action) {
+ is HomeScreenAction.UpdateSelectedProject -> {
+ _uiState.update { it.copy(selectedProject = action.project) }
+ }
+
+ is HomeScreenAction.AddMediaClicked -> TODO()
+ }
+ }
+
+ private fun loadSpacesAndFolders() {
+ viewModelScope.launch {
+ val allSpaces = Space.getAll().asSequence().toList()
+ val selectedSpace = Space.current
+ val projectsForSelectedSpace = selectedSpace?.projects ?: emptyList()
+
+ _uiState.update {
+ it.copy(
+ allSpaces = allSpaces,
+ projectsForSelectedSpace = projectsForSelectedSpace,
+ selectedSpace = selectedSpace,
+ selectedProject = projectsForSelectedSpace.firstOrNull()
+ )
+ }
+ }
+ }
+
+}
+
+sealed class HomeScreenAction {
+ data class UpdateSelectedProject(val project: Project? = null) : HomeScreenAction()
+ data class AddMediaClicked(val mediaType: AddMediaType): HomeScreenAction()
+}
+
+data class HomeScreenState(
+ val selectedSpace: Space? = null,
+ val selectedProject: Project? = null,
+ val allSpaces: List = emptyList(),
+ val projectsForSelectedSpace: List = emptyList()
+)
+
+@Composable
+fun HomeScreenContent(
+ onExit: () -> Unit,
+ state: HomeScreenState,
+ onAction: (HomeScreenAction) -> Unit,
+ onNavigateToCache: () -> Unit = {}
+) {
+
+ val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
+ val scope = rememberCoroutineScope()
+
+ val projects = state.projectsForSelectedSpace
+ val totalPages = max(1, projects.size) + 1
+ val pagerState = rememberPagerState(initialPage = 0) { totalPages }
+
+ val currentProjectIndex = state.selectedProject?.let { selected ->
+ projects.indexOfFirst { it.id == selected.id }.takeIf { it >= 0 } ?: 0
+ } ?: 0
+
+ // Whenever the pager’s current page changes and it represents a project page,
+ // update the view model’s selected project.
+ LaunchedEffect(pagerState.currentPage, projects) {
+ if (projects.isNotEmpty() && pagerState.currentPage < projects.size) {
+ val newlySelectedProject = projects[pagerState.currentPage]
+ onAction(HomeScreenAction.UpdateSelectedProject(newlySelectedProject))
+ }
+ }
+
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+
+ ModalNavigationDrawer(
+ drawerState = drawerState,
+ gesturesEnabled = true,
+ drawerContent = {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
+ MainDrawerContent(
+ selectedSpace = state.selectedSpace,
+ spaceList = state.allSpaces
+ )
+ }
+ }
+ ) {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
+
+ Scaffold(
+ topBar = {
+ HomeAppBar(
+ onExit = onExit,
+ openDrawer = {
+ scope.launch {
+ drawerState.open()
+ }
+ }
+ )
+ },
+
+ bottomBar = {
+ MainBottomBar(
+ isSettings = pagerState.currentPage == (totalPages - 1),
+ onAddMediaClick = {},
+ onMyMediaClick = {
+ // When "My Media" is tapped, scroll to the page of the currently selected project.
+ // If no project is selected, default to the first page.
+ val targetPage = if (projects.isEmpty()) 0 else currentProjectIndex
+ if (pagerState.currentPage != targetPage) {
+ scope.launch { pagerState.scrollToPage(targetPage) }
+ }
+ },
+ onSettingsClick = {
+ // Scroll to the last page if not already there.
+ if (pagerState.currentPage != totalPages - 1) {
+ scope.launch { pagerState.scrollToPage(totalPages - 1) }
+ }
+ }
+ )
+ }
+
+ ) { paddingValues ->
+
+ Column(
+ modifier = Modifier.padding(paddingValues)
+ ) {
+ AnimatedVisibility(
+ visible = pagerState.currentPage < totalPages - 1,
+ enter = slideInHorizontally(
+ animationSpec = tween()
+ ),
+ exit = slideOutHorizontally(
+ animationSpec = tween()
+ )
+ ) {
+ val selectedProject =
+ state.selectedProject ?: error("Project should not be empty")
+ val selectedSpace =
+ state.selectedSpace ?: error("Space should not be empty")
+
+ val folderName = selectedProject.description
+ ?: selectedProject.created.toString()
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = dimensionResource(R.dimen.activity_horizontal_margin)),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row {
+ SpaceIcon(
+ type = selectedSpace.tType ?: Space.Type.WEBDAV,
+ modifier = Modifier.size(24.dp)
+ )
+ Icon(
+ painter = painterResource(R.drawable.keyboard_arrow_right),
+ contentDescription = null
+ )
+ Text(folderName)
+ }
+
+
+ TextButton(
+ onClick = {}
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_edit_folder),
+ contentDescription = null
+ )
+ Text("Edit")
+ }
+ }
+ }
+
+
+
+ HorizontalPager(
+ state = pagerState,
+ modifier = Modifier.fillMaxSize(),
+ ) { page ->
+
+ when (page) {
+ 0 -> {
+ // First page: If no projects, show -1, else show first project's ID
+ MainMediaScreen(projectId = if (projects.isEmpty()) -1 else projects[0].id)
+ }
+
+ in 1 until projects.size -> {
+ // Next project IDs (page - 1)
+ MainMediaScreen(projects[page].id)
+ }
+
+ totalPages - 1 -> {
+ // Always settings screen as the last page
+ SettingsScreen(
+ onNavigateToCache = onNavigateToCache
+ )
+ }
+
+ else -> {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text("Unexpected page index")
+ }
+ } // This should never be reached
+ }
+ }
+
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun MainContentPreview() {
+ SaveAppTheme {
+
+ HomeScreenContent(
+ onExit = {},
+ state = HomeScreenState(),
+ onAction = {}
+ )
+ }
+}
+
+
+//@Composable
+//fun MainMediaScreen(projectId: Long) {
+//
+// val fragmentState = rememberFragmentState()
+//
+// AndroidFragment(
+// modifier = Modifier.fillMaxSize(),
+// fragmentState = fragmentState,
+// arguments = bundleOf("project_id" to projectId),
+// onUpdate = {
+// //
+// }
+// )
+//}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaScreen.kt
new file mode 100644
index 00000000..64a64207
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaScreen.kt
@@ -0,0 +1,414 @@
+package net.opendasharchive.openarchive.features.main.ui
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Handler
+import android.os.Looper
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.interaction.MutableInteractionSource
+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.aspectRatio
+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.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Error
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import coil3.compose.AsyncImage
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import net.opendasharchive.openarchive.db.Collection
+import net.opendasharchive.openarchive.db.Media
+import net.opendasharchive.openarchive.features.media.PreviewActivity
+import net.opendasharchive.openarchive.upload.BroadcastManager
+import net.opendasharchive.openarchive.upload.UploadManagerActivity
+import org.koin.androidx.compose.koinViewModel
+
+/**
+ * A data class representing one “section” (i.e. one Collection and its list of Media).
+ * (Here we wrap the list of media in a mutableStateListOf so that updates trigger recomposition.)
+ */
+data class CollectionSection(
+ val collection: Collection,
+ val media: SnapshotStateList = mutableStateListOf().apply { addAll(collection.media) }
+)
+
+@Composable
+fun MainMediaScreen(
+ projectId: Long,
+) {
+ val context = LocalContext.current
+
+ // State holding our list of sections (each collection with its media)
+ val sections = remember { mutableStateListOf() }
+ // Flag to track if any media is “selected” (for deletion)
+ var isSelecting by remember { mutableStateOf(false) }
+ // State to control showing the “delete confirmation” dialog.
+ var showDeleteDialog by remember { mutableStateOf(false) }
+ // State to control showing an error/retry dialog for a media item.
+ var errorDialogData by remember { mutableStateOf(null) }
+
+
+ // Handle broadcast messages
+ DisposableEffect(Unit) {
+ val handler = Handler(Looper.getMainLooper())
+ val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val action = BroadcastManager.getAction(intent) ?: return
+ when (action) {
+ BroadcastManager.Action.Change -> {
+ // Extract extras from the intent (assuming these keys are provided)
+ val collectionId = intent.getLongExtra("collectionId", -1)
+ val mediaId = intent.getLongExtra("mediaId", -1)
+ val progress = intent.getIntExtra("progress", 0)
+ val isUploaded = intent.getBooleanExtra("isUploaded", false)
+ if (collectionId != -1L && mediaId != -1L) {
+ handler.post {
+ updateMediaItem(
+ sections = sections,
+ collectionId = collectionId,
+ mediaId = mediaId,
+ progress = progress,
+ isUploaded = isUploaded
+ )
+ }
+ }
+ }
+
+ BroadcastManager.Action.Delete -> {
+ handler.post { refreshSections(projectId, sections) }
+ }
+ }
+ }
+ }
+
+ BroadcastManager.register(context, receiver)
+ onDispose { BroadcastManager.unregister(context, receiver) }
+ }
+
+ LaunchedEffect(projectId) {
+ refreshSections(projectId, sections)
+ }
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (sections.isEmpty()) {
+ WelcomeMessage()
+ } else {
+ // Use a LazyColumn to list each collection section vertically.
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(sections, key = { it.collection.id }) { section ->
+ CollectionSectionView(
+ section = section,
+ onMediaClick = { media ->
+ handleMediaClick(context, media) { errorMedia ->
+ errorDialogData = errorMedia
+ }
+ },
+ onMediaLongPress = { media ->
+ // For selection (if needed)
+ toggleMediaSelection(media)
+ }
+ )
+ }
+ }
+ }
+
+ // Add floating action button or other UI elements if needed
+ }
+}
+
+/** Shows a header with the collection’s upload date and media count */
+@Composable
+fun CollectionHeaderView(section: CollectionSection) {
+ // For example, showing date and item count side by side:
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val dateText = section.collection.uploadDate?.toGMTString() ?: "Unknown Date"
+ Text(text = dateText, style = MaterialTheme.typography.titleMedium)
+ Text(
+ text = "${section.media.size} items",
+ style = MaterialTheme.typography.bodyMedium,
+ color = Color.Gray
+ )
+ }
+}
+
+/** Renders one collection section: header and grid of media items. */
+@Composable
+fun CollectionSectionView(
+ section: CollectionSection,
+ onMediaClick: (Media) -> Unit,
+ onMediaLongPress: (Media) -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp)
+ ) {
+ CollectionHeaderView(section)
+ // Render the media items as a grid of 4 columns.
+ // We use a simple approach: chunk the media list into rows of 4.
+ val rows = section.media.chunked(4)
+ rows.forEach { rowItems ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ rowItems.forEach { media ->
+ MediaItemView(
+ media = media,
+ isSelected = media.selected,
+ onClick = { onMediaClick(media) },
+ onLongClick = { onMediaLongPress(media) },
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ )
+ }
+ // Fill out the remaining cells (if any) in this row
+ if (rowItems.size < 4) {
+ repeat(4 - rowItems.size) {
+ Spacer(modifier = Modifier.weight(1f))
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(4.dp))
+ }
+ }
+}
+
+/** Renders one media item as an image filling its box. */
+@Composable
+fun MediaItemView(
+ media: Media,
+ isSelected: Boolean,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .border(
+ width = if (isSelected) 4.dp else 0.dp,
+ color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent
+ )
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onTap = { onClick() },
+ onLongPress = { onLongClick() }
+ )
+ }
+ ) {
+ AsyncImage(
+ model = media.fileUri,
+ contentDescription = media.title,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ when (media.sStatus) {
+ Media.Status.Uploading -> UploadProgress(media.uploadPercentage ?: 0)
+ Media.Status.Error -> ErrorIndicator()
+ else -> Unit
+ }
+ }
+}
+
+
+@Composable
+fun UploadProgress(progress: Int) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black.copy(alpha = 0.6f)),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(
+ progress = progress / 100f,
+ modifier = Modifier.size(48.dp),
+ color = Color.White
+ )
+ Text(
+ text = "$progress%",
+ color = Color.White,
+ modifier = Modifier.padding(top = 56.dp)
+ )
+ }
+}
+
+@Composable
+fun ErrorIndicator() {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Red.copy(alpha = 0.6f)),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Default.Error,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(48.dp)
+ )
+ }
+}
+
+@Composable
+fun WelcomeMessage() {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "Welcome",
+ style = MaterialTheme.typography.displayMedium
+ )
+ Text(
+ text = "Tap the button below to add media",
+ style = MaterialTheme.typography.titleMedium
+ )
+ }
+}
+
+/** Refreshes the list of collections (with nonempty media) for the given project.
+ * This runs on IO and updates the [sections] state on the main thread.
+ */
+private fun refreshSections(projectId: Long, sections: MutableList) {
+ kotlinx.coroutines.GlobalScope.launch(Dispatchers.IO) {
+ val collections = Collection.getByProject(projectId)
+ val newSections = collections.filter { it.media.isNotEmpty() }
+ .map { CollectionSection(it) }
+ withContext(Dispatchers.Main) {
+ sections.clear()
+ sections.addAll(newSections)
+ }
+ }
+}
+
+
+/** Updates one media item in one section (called when a broadcast “change” is received). */
+private fun updateMediaItem(
+ sections: List,
+ collectionId: Long,
+ mediaId: Long,
+ progress: Int,
+ isUploaded: Boolean
+) {
+ sections.find { it.collection.id == collectionId }?.let { section ->
+ val idx = section.media.indexOfFirst { it.id == mediaId }
+ if (idx != -1) {
+ val media = section.media[idx]
+ if (isUploaded) {
+ media.status = Media.Status.Uploaded.id
+ } else {
+ media.uploadPercentage = progress
+ media.status = Media.Status.Uploading.id
+ }
+ // Replace to trigger recomposition
+ section.media[idx] = media
+ }
+ }
+}
+
+/** Toggles the selected state of the media item and saves it. */
+private fun toggleMediaSelection(media: Media) {
+ media.selected = !media.selected
+ media.save()
+}
+
+/** Deletes any media items that are selected from all sections.
+ * Also deletes the media from the database and posts a delete broadcast.
+ */
+private fun deleteSelected(sections: MutableList, context: Context) {
+ sections.forEach { section ->
+ // Work on a copy so we can remove items safely
+ section.media.filter { it.selected }.toList().forEach { media ->
+ section.media.remove(media)
+ media.delete() // delete from database
+ BroadcastManager.postDelete(context, media.id)
+ }
+ }
+ // Remove sections that are now empty (do not delete the collection from DB here)
+ sections.removeAll { it.media.isEmpty() }
+}
+
+/** Deletes a single media item (used when “remove” is chosen from the error dialog). */
+private fun deleteMediaItem(sections: MutableList, media: Media) {
+ sections.find { it.collection.id == media.collectionId }?.let { section ->
+ section.media.remove(media)
+ media.delete()
+ // In a real app, you might also post a broadcast here
+ }
+}
+
+/** Handles what happens when a media item is clicked (when not in selection mode).
+ * Depending on its status and mime type, this either launches a preview, an upload manager,
+ * or shows an error dialog.
+ *
+ * The onError lambda is called if the media is in an error state.
+ */
+private fun handleMediaClick(context: Context, media: Media, onError: (Media) -> Unit) {
+ when (media.sStatus) {
+ Media.Status.Local -> {
+ // For images, start a preview
+ if (media.mimeType.startsWith("image")) {
+ PreviewActivity.start(context, media.projectId)
+ }
+ }
+
+ Media.Status.Queued, Media.Status.Uploading -> {
+ // Start the upload manager activity
+ context.startActivity(Intent(context, UploadManagerActivity::class.java))
+ }
+
+ Media.Status.Error -> {
+ // Show error dialog (retry/remove)
+ onError(media)
+ }
+
+ else -> { /* no op */
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaViewModel.kt
new file mode 100644
index 00000000..30e0cb08
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaViewModel.kt
@@ -0,0 +1,8 @@
+package net.opendasharchive.openarchive.features.main.ui
+
+import androidx.lifecycle.ViewModel
+
+class MainMediaViewModel : ViewModel() {
+
+}
+
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MediaCacheScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MediaCacheScreen.kt
new file mode 100644
index 00000000..b36287c1
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MediaCacheScreen.kt
@@ -0,0 +1,178 @@
+package net.opendasharchive.openarchive.features.main.ui
+
+import android.content.Context
+import android.os.Bundle
+import android.provider.MediaStore
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Description
+import androidx.compose.material.icons.filled.Folder
+import androidx.compose.material.icons.filled.Image
+import androidx.compose.material.icons.filled.Movie
+import androidx.compose.material.icons.filled.QuestionMark
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import coil3.compose.AsyncImage
+import coil3.request.ImageRequest
+import coil3.request.crossfade
+import coil3.size.Scale
+import java.io.File
+
+// MediaFile Data Class
+data class MediaFile(
+ val name: String,
+ val path: String,
+ val isDirectory: Boolean,
+ val type: FileType
+)
+
+// Enum to represent different file types
+enum class FileType {
+ IMAGE, VIDEO, PDF, FOLDER, UNKNOWN
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MediaCacheScreen(context: Context, onNavigateBack: () -> Unit) {
+ val cacheDir = context.cacheDir
+ val files = remember { cacheDir.listFiles()?.map { it.toMediaFile() } ?: emptyList() }
+
+ Scaffold(
+topBar ={
+ TopAppBar(
+ title = { Text("Media Cache") },
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(Icons.Default.ArrowBack, contentDescription = null)
+ }
+ }
+ )
+}
+
+ ) { paddingValues ->
+
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)) {
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = 100.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White),
+ contentPadding = PaddingValues(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(files) { file ->
+ CacheFileItem(file)
+ }
+ }
+ }
+ }
+
+}
+
+@Composable
+fun CacheFileItem(file: MediaFile) {
+ val context = LocalContext.current
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .background(Color.LightGray)
+ .padding(8.dp)
+ ) {
+ when {
+ file.isDirectory -> {
+ Icon(
+ imageVector = Icons.Default.Folder,
+ contentDescription = file.name,
+ modifier = Modifier.size(48.dp)
+ )
+ }
+
+ file.type == FileType.IMAGE -> {
+ AsyncImage(
+ model = ImageRequest.Builder(context)
+ .data(File(file.path))
+ .scale(Scale.FILL)
+ .crossfade(true)
+ .build(),
+ contentDescription = file.name,
+ modifier = Modifier.size(64.dp),
+ contentScale = ContentScale.Crop
+ )
+ }
+
+ file.type == FileType.VIDEO -> {
+ AsyncImage(
+ model = ImageRequest.Builder(context)
+ .data(File(file.path))
+ .scale(Scale.FIT)
+ .crossfade(true)
+ .build(),
+ contentDescription = file.name,
+ modifier = Modifier.size(64.dp),
+ contentScale = ContentScale.Crop
+ )
+ }
+
+ file.type == FileType.PDF -> {
+ Icon(
+ imageVector = Icons.Default.Description,
+ contentDescription = file.name,
+ modifier = Modifier.size(48.dp)
+ )
+ }
+
+ else -> {
+ Icon(
+ imageVector = Icons.Default.QuestionMark,
+ contentDescription = file.name,
+ modifier = Modifier.size(48.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = file.name,
+ maxLines = 1,
+ modifier = Modifier.widthIn(max = 80.dp)
+ )
+ }
+}
+
+
+fun File.toMediaFile(): MediaFile {
+ val fileType = when {
+ isDirectory -> FileType.FOLDER
+ name.endsWith(".jpg", true) || name.endsWith(".jpeg", true) || name.endsWith(".png", true) -> FileType.IMAGE
+ name.endsWith(".mp4", true) || name.endsWith(".mkv", true) || name.endsWith(".avi", true) -> FileType.VIDEO
+ name.endsWith(".pdf", true) -> FileType.PDF
+ else -> FileType.UNKNOWN
+ }
+ return MediaFile(name = name, path = absolutePath, isDirectory = isDirectory, type = fileType)
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/ExpandableSpaceList.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/ExpandableSpaceList.kt
new file mode 100644
index 00000000..820ea93d
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/ExpandableSpaceList.kt
@@ -0,0 +1,171 @@
+package net.opendasharchive.openarchive.features.main.ui.components
+
+import android.content.res.Configuration
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.outlined.KeyboardArrowDown
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.presentation.components.PrimaryButton
+import net.opendasharchive.openarchive.core.presentation.theme.DefaultBoxPreview
+import net.opendasharchive.openarchive.db.Space
+import net.opendasharchive.openarchive.features.core.Accordion
+import net.opendasharchive.openarchive.features.core.AccordionState
+import net.opendasharchive.openarchive.features.core.rememberAccordionState
+
+@Composable
+fun ExpandableSpaceList(
+ serverAccordionState: AccordionState,
+ selectedSpace: Space? = null,
+ spaceList: List
+) {
+ Accordion(
+ state = serverAccordionState,
+ headerContent = {
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+
+ if (selectedSpace != null) {
+ DrawerSpaceListItem(space = selectedSpace)
+ } else {
+ Text("Servers")
+ }
+
+ IconButton(
+ modifier = Modifier.rotate(serverAccordionState.animationProgress * 180),
+ onClick = {
+ serverAccordionState.toggle()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.KeyboardArrowDown,
+ contentDescription = "Expand"
+ )
+ }
+ }
+ },
+ bodyContent = {
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ spaceList.forEach { space ->
+ DrawerSpaceListItem(space)
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ PrimaryButton(
+ text = "Add Server",
+ icon = Icons.Default.Add
+ ) { }
+ }
+ }
+
+ }
+ )
+}
+
+@Composable
+fun DrawerSpaceListItem(
+ space: Space,
+) {
+ Row(
+ modifier = Modifier
+ .wrapContentSize()
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ SpaceIcon(
+ type = space.tType ?: Space.Type.INTERNET_ARCHIVE,
+ modifier = Modifier.size(24.dp)
+ )
+
+ Text(space.name)
+ }
+}
+
+@Composable
+fun SpaceIcon(
+ type: Space.Type,
+ modifier: Modifier = Modifier,
+ tint: Color? = null
+) {
+ val icon = when (type) {
+ Space.Type.WEBDAV -> painterResource(R.drawable.ic_space_private_server)
+ Space.Type.INTERNET_ARCHIVE -> painterResource(R.drawable.ic_space_interent_archive)
+ Space.Type.GDRIVE -> painterResource(R.drawable.logo_gdrive_outline)
+ Space.Type.RAVEN -> painterResource(R.drawable.ic_space_dweb)
+ }
+ Icon(
+ modifier = modifier,
+ painter = icon,
+ contentDescription = null,
+ tint = tint ?: MaterialTheme.colorScheme.onBackground
+ )
+}
+
+@Preview(uiMode = UI_MODE_NIGHT_YES)
+@Composable
+private fun ExpandableSpaceListPreview() {
+ val state = rememberAccordionState(
+ expanded = true,
+ )
+
+ DefaultBoxPreview {
+ ExpandableSpaceList(
+ selectedSpace = dummySpaceList[1],
+ spaceList = dummySpaceList,
+ serverAccordionState = state
+ )
+ }
+}
+
+val dummySpaceList = listOf(
+ Space(
+ type = Space.Type.WEBDAV.id,
+ username = "",
+ password = "",
+ name = "Elelan Server",
+ ),
+ Space(
+ type = Space.Type.INTERNET_ARCHIVE.id,
+ username = "",
+ password = "",
+ name = "Test Server",
+ ),
+ Space(
+ type = Space.Type.RAVEN.id,
+ username = "",
+ password = "",
+ name = "DWebServer",
+ ),
+)
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/FolderOptionsPopup.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/FolderOptionsPopup.kt
new file mode 100644
index 00000000..7985a7b8
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/FolderOptionsPopup.kt
@@ -0,0 +1,59 @@
+package net.opendasharchive.openarchive.features.main.ui.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun FolderOptionsPopup(
+ expanded: Boolean = false,
+ onDismissRequest: () -> Unit,
+ onRenameFolder: () -> Unit,
+ onSelectMedia: () -> Unit,
+ onRemoveFolder: () -> Unit
+) {
+
+ DropdownMenu(
+ modifier = Modifier,
+ expanded = expanded,
+ onDismissRequest = onDismissRequest
+ ) {
+
+ Column(modifier = Modifier.padding(8.dp)) {
+
+ DropdownMenuItem(
+ onClick = onRenameFolder,
+ text = { Text("Rename Folder") }
+ )
+ DropdownMenuItem(
+ onClick = onSelectMedia,
+ text = { Text("Select Media") }
+ )
+ DropdownMenuItem(
+ onClick = onRemoveFolder,
+ text = { Text("Remove Folder") }
+ )
+ }
+ }
+
+}
+
+@Preview
+@Composable
+private fun FolderOptionsPopupPreview() {
+
+ FolderOptionsPopup(
+ expanded = true,
+ onDismissRequest = {},
+ onRenameFolder = {},
+ onSelectMedia = {},
+ onRemoveFolder = {}
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/HomeAppBar.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/HomeAppBar.kt
new file mode 100644
index 00000000..3d092349
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/HomeAppBar.kt
@@ -0,0 +1,79 @@
+package net.opendasharchive.openarchive.features.main.ui.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import net.opendasharchive.openarchive.R
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun HomeAppBar(
+ openDrawer: () -> Unit,
+ onExit: () -> Unit
+) {
+
+ TopAppBar(
+ title = {
+ Image(
+ modifier = Modifier
+ .size(64.dp)
+ .clickable {
+ onExit()
+ },
+ painter = painterResource(R.drawable.savelogo),
+ contentDescription = "Save Logo",
+ colorFilter = ColorFilter.tint(colorResource(R.color.colorOnPrimary))
+ )
+ },
+ actions = {
+
+ AnimatedVisibility(
+ visible = false
+ ) {
+ IconButton(
+ onClick = {}
+ ) {
+ Icon(
+ Icons.Outlined.Delete,
+ contentDescription = null
+ )
+ }
+
+ }
+
+ IconButton(
+ colors = IconButtonDefaults.iconButtonColors(
+ contentColor = colorResource(R.color.colorOnSecondary)
+ ),
+ onClick = {
+ openDrawer()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Default.Menu,
+ contentDescription = null
+ )
+ }
+
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = colorResource(R.color.colorPrimary)
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/MainBottomBar.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/MainBottomBar.kt
new file mode 100644
index 00000000..494685ac
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/MainBottomBar.kt
@@ -0,0 +1,97 @@
+package net.opendasharchive.openarchive.features.main.ui.components
+
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.PermMedia
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.outlined.PermMedia
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.unit.dp
+import net.opendasharchive.openarchive.R
+
+@Composable
+fun MainBottomBar(
+ isSettings: Boolean,
+ onMyMediaClick: () -> Unit,
+ onSettingsClick: () -> Unit,
+ onAddMediaClick: () -> Unit
+) {
+ NavigationBar(
+ modifier = Modifier.fillMaxWidth(),
+ containerColor = MaterialTheme.colorScheme.primary
+ ) {
+
+ BottomNavMenuItem(
+ isSelected = !isSettings,
+ onClick = onMyMediaClick,
+ selectedIcon = Icons.Default.PermMedia,
+ unSelectedIcon = Icons.Outlined.PermMedia,
+ text = "My Media"
+ )
+
+ FloatingActionButton(
+ modifier = Modifier.size(height = 42.dp, width = 90.dp),
+ onClick = onAddMediaClick,
+ containerColor = colorResource(R.color.colorOnPrimary),
+ shape = RoundedCornerShape(percent = 50),
+ elevation = FloatingActionButtonDefaults.elevation(
+ defaultElevation = 6.dp,
+ pressedElevation = 12.dp
+ )
+ ) {
+ Icon(
+ modifier = Modifier.size(28.dp),
+ imageVector = Icons.Default.Add,
+ contentDescription = null
+ )
+ }
+
+ BottomNavMenuItem(
+ isSelected = isSettings,
+ onClick = onSettingsClick,
+ selectedIcon = Icons.Default.Settings,
+ unSelectedIcon = Icons.Outlined.Settings,
+ text = "Settings"
+ )
+
+ }
+}
+
+@Composable
+fun RowScope.BottomNavMenuItem(
+ selectedIcon: ImageVector,
+ unSelectedIcon: ImageVector,
+ isSelected: Boolean,
+ text: String,
+ onClick: () -> Unit
+) {
+ val icon = if (isSelected) selectedIcon else unSelectedIcon
+ NavigationBarItem(
+ label = {
+ Text(text)
+ },
+ selected = isSelected,
+ onClick = onClick,
+ icon = {
+ Icon(
+ imageVector = icon,
+ contentDescription = null
+ )
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/MainDrawerContent.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/MainDrawerContent.kt
new file mode 100644
index 00000000..c96f535a
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/MainDrawerContent.kt
@@ -0,0 +1,219 @@
+package net.opendasharchive.openarchive.features.main.ui.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Folder
+import androidx.compose.material.icons.outlined.Folder
+import androidx.compose.material3.Button
+import androidx.compose.material3.DrawerDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalDrawerSheet
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview
+import net.opendasharchive.openarchive.db.Project
+import net.opendasharchive.openarchive.db.Space
+import net.opendasharchive.openarchive.features.core.rememberAccordionState
+
+@Composable
+fun MainDrawerContent(
+ selectedSpace: Space? = null,
+ spaceList: List = emptyList()
+) {
+
+ val configuration = LocalConfiguration.current
+ val screenWidth = configuration.screenWidthDp.dp
+
+ val serverAccordionState = rememberAccordionState()
+
+ ModalDrawerSheet(
+ drawerShape = DrawerDefaults.shape,
+ modifier = Modifier.width(screenWidth * 0.65f),
+ drawerContainerColor = Color.White
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(vertical = 24.dp),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+
+ Column(
+ modifier = Modifier
+ .padding(vertical = 24.dp)
+ .verticalScroll(rememberScrollState()),
+ ) {
+
+
+ Spacer(Modifier.height(12.dp))
+
+ ExpandableSpaceList(
+ serverAccordionState,
+ selectedSpace = selectedSpace,
+ spaceList = spaceList
+ )
+
+ HorizontalDivider(
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ thickness = 0.3.dp,
+ modifier = Modifier.padding(vertical = 24.dp)
+ )
+
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ Icon(
+ imageVector = Icons.Default.Folder,
+ tint = MaterialTheme.colorScheme.primary,
+ contentDescription = null
+ )
+ Text("Summer Vacation")
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ Icon(
+ imageVector = Icons.Outlined.Folder,
+ tint = MaterialTheme.colorScheme.onBackground,
+ contentDescription = null
+ )
+ Text("Prague")
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ Icon(
+ imageVector = Icons.Outlined.Folder,
+ tint = MaterialTheme.colorScheme.onBackground,
+ contentDescription = null
+ )
+ Text("Misc")
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ Icon(
+ imageVector = Icons.Outlined.Folder,
+ tint = MaterialTheme.colorScheme.onBackground,
+ contentDescription = null
+ )
+ Text("Folder")
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ Icon(
+ imageVector = Icons.Outlined.Folder,
+ tint = MaterialTheme.colorScheme.onBackground,
+ contentDescription = null
+ )
+ Text("Folder")
+ }
+ }
+
+
+
+ Spacer(Modifier.height(12.dp))
+
+
+ }
+
+
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
+
+ Button(
+ modifier = Modifier.fillMaxWidth(0.7f),
+ shape = RoundedCornerShape(8f),
+ onClick = {
+
+ }
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(imageVector = Icons.Default.Add, contentDescription = null)
+ Text("New Folder")
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun MainDrawerFolderListItem(
+ project: Project,
+ isSelected: Boolean = false,
+ onSelected: () -> Unit
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ Icon(
+ imageVector = Icons.Outlined.Folder,
+ tint = MaterialTheme.colorScheme.onBackground,
+ contentDescription = null
+ )
+
+ Text("Prague")
+ }
+}
+
+@Preview
+@Composable
+private fun MainDrawerContentPreview() {
+ DefaultScaffoldPreview {
+ MainDrawerContent()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewActivity.kt
index 1298ce19..082d4505 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewActivity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewActivity.kt
@@ -231,6 +231,12 @@ class PreviewActivity : BaseActivity(), View.OnClickListener, PreviewAdapter.Lis
icon = R.drawable.perm_media_24px.asUiImage()
title = R.string.edit_multiple.asUiText()
message = R.string.press_and_hold_to_select_and_edit_multiple_media.asUiText()
+ positiveButton {
+ text = UiText.StringResource(R.string.lbl_got_it)
+ action = {
+ dialogManager.dismissDialog()
+ }
+ }
}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewAdapter.kt
index 591d7407..2768c210 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewAdapter.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/media/PreviewAdapter.kt
@@ -1,14 +1,16 @@
package net.opendasharchive.openarchive.features.media
+import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
+import net.opendasharchive.openarchive.databinding.RvMediaBoxBinding
import net.opendasharchive.openarchive.db.Media
-import net.opendasharchive.openarchive.db.MediaViewHolder
+import net.opendasharchive.openarchive.features.media.adapter.PreviewViewHolder
import java.lang.ref.WeakReference
-class PreviewAdapter(listener: Listener? = null): ListAdapter(DIFF_CALLBACK) {
+class PreviewAdapter(listener: Listener? = null): ListAdapter(DIFF_CALLBACK) {
interface Listener {
@@ -53,8 +55,9 @@ class PreviewAdapter(listener: Listener? = null): ListAdapter
val media = getMedia(view) ?: return@setOnClickListener
@@ -79,7 +82,7 @@ class PreviewAdapter(listener: Listener? = null): ListAdapter()
+ }
+
+ private val mContext = itemView.context
+
+ private val mPicasso = Picasso.Builder(mContext)
+ .addRequestHandler(VideoRequestHandler(mContext))
+ .build()
+
+
+ @SuppressLint("SetTextI18n")
+ fun bind(media: Media? = null, batchMode: Boolean = false, doImageFade: Boolean = true) {
+ itemView.tag = media?.id
+ if (batchMode && media?.selected == true) {
+ itemView.setBackgroundResource(R.color.colorPrimary)
+ binding.selectedIndicator.show()
+ } else {
+ itemView.setBackgroundResource(R.color.transparent)
+ binding.selectedIndicator.hide()
+ }
+
+ binding.image.alpha = if (media?.sStatus == Media.Status.Uploaded || !doImageFade) 1f else 0.5f
+
+ if (media?.mimeType?.startsWith("image") == true) {
+ val progress = CircularProgressDrawable(mContext)
+ progress.strokeWidth = 5f
+ progress.centerRadius = 30f
+ progress.start()
+
+ Glide.with(mContext)
+ .load(media.fileUri)
+ .placeholder(progress)
+ .fitCenter()
+ .into(binding.image)
+
+ binding.image.show()
+ binding.waveform.hide()
+ binding.videoIndicator.hide()
+ } else if (media?.mimeType?.startsWith("video") == true) {
+ mPicasso.load(VideoRequestHandler.SCHEME_VIDEO + ":" + media.originalFilePath)
+ .fit()
+ .centerCrop()
+ .into(binding.image)
+
+ binding.image.show()
+ binding.waveform.hide()
+ binding.videoIndicator.show()
+ } else if (media?.mimeType?.startsWith("audio") == true) {
+ binding.videoIndicator.hide()
+
+ val soundFile = soundCache[media.originalFilePath]
+
+ if (soundFile != null) {
+ binding.image.hide()
+ binding.waveform.setAudioFile(soundFile)
+ binding.waveform.show()
+ } else {
+ binding.image.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.no_thumbnail))
+ binding.image.show()
+ binding.waveform.hide()
+
+ CoroutineScope(Dispatchers.IO).launch {
+ @Suppress("NAME_SHADOWING")
+ val soundFile = try {
+ SoundFile.create(media.originalFilePath) {
+ return@create true
+ }
+ } catch (e: Throwable) {
+ Timber.d(e)
+
+ null
+ }
+
+ if (soundFile != null) {
+ soundCache[media.originalFilePath] = soundFile
+
+ MainScope().launch {
+ binding.waveform.setAudioFile(soundFile)
+ binding.image.hide()
+ binding.waveform.show()
+ }
+ }
+ }
+ }
+ } else {
+ binding.image.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.no_thumbnail))
+ binding.image.show()
+ binding.waveform.hide()
+ binding.videoIndicator.hide()
+ }
+
+ if (media != null) {
+
+
+ val sbTitle = StringBuffer()
+
+ when (media.sStatus) {
+ Media.Status.Error -> {
+ AppLogger.i("Media Item ${media.id} is error")
+ sbTitle.append(mContext.getString(R.string.error))
+
+ binding.overlayContainer.show()
+ binding.progress.hide()
+ binding.progressText.hide()
+ binding.error.show()
+
+ }
+ Media.Status.Queued -> {
+ AppLogger.i("Media Item ${media.id} is queued")
+ binding.overlayContainer.show()
+ binding.progress.isIndeterminate = true
+ binding.progress.show()
+ binding.progressText.hide()
+ binding.error.hide()
+ }
+ Media.Status.Uploading -> {
+ binding.progress.isIndeterminate = false
+ val progressValue = media.uploadPercentage ?: 0
+ AppLogger.i("Media Item ${media.id} is uploading")
+
+ binding.overlayContainer.show()
+ binding.progress.show()
+ binding.progressText.show()
+
+ // Make sure to keep spinning until the upload has made some noteworthy progress.
+ if (progressValue > 2) {
+ binding.progress.setProgressCompat(progressValue, true)
+ }
+
+ binding.progressText.text = "${progressValue}%"
+
+ binding.error.hide()
+ }
+ else -> {
+ binding.overlayContainer.hide()
+ binding.progress.hide()
+ binding.progressText.hide()
+ binding.error.hide()
+ }
+ }
+
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23InstructionsActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23InstructionsActivity.kt
index 49e8f42e..5b9054f5 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23InstructionsActivity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/Onboarding23InstructionsActivity.kt
@@ -99,10 +99,10 @@ class Onboarding23InstructionsActivity : BaseActivity() {
private fun updateCoverImage() {
when (mBinding.viewPager.currentItem) {
- 0 -> mBinding.coverImage.setImageResource(R.drawable.onboarding23_cover_secure)
- 1 -> mBinding.coverImage.setImageResource(R.drawable.onboarding23_cover_archive)
- 2 -> mBinding.coverImage.setImageResource(R.drawable.onboarding23_cover_verify)
- 3 -> mBinding.coverImage.setImageResource(R.drawable.onboarding23_cover_encrypt)
+ 0 -> mBinding.coverImage.setImageResource(R.drawable.onboarding_secure_png)
+ 1 -> mBinding.coverImage.setImageResource(R.drawable.onboarding_archive_png)
+ 2 -> mBinding.coverImage.setImageResource(R.drawable.onboarding_verify_png)
+ 3 -> mBinding.coverImage.setImageResource(R.drawable.onboarding_encrypt_png)
}
mBinding.coverImage.alpha = 0F
mBinding.coverImage.animate().setDuration(200L).alpha(1F).start()
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 81e16511..803dca7c 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
@@ -3,6 +3,16 @@ package net.opendasharchive.openarchive.features.onboarding
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavController
+import androidx.navigation.NavGraph
+import androidx.navigation.NavOptions
+import androidx.navigation.Navigator
+import androidx.navigation.findNavController
+import androidx.navigation.fragment.FragmentNavigator
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.ui.AppBarConfiguration
+import androidx.navigation.ui.setupActionBarWithNavController
import net.opendasharchive.openarchive.R
import net.opendasharchive.openarchive.databinding.ActivitySpaceSetupBinding
import net.opendasharchive.openarchive.extensions.onBackButtonPressed
@@ -24,53 +34,81 @@ import net.opendasharchive.openarchive.services.snowbird.SnowbirdShareFragment
import net.opendasharchive.openarchive.services.webdav.WebDavFragment
import net.opendasharchive.openarchive.services.webdav.WebDavSetupLicenseFragment
+enum class StartDestination {
+ SPACE_TYPE,
+ SPACE_LIST,
+ DWEB_DASHBOARD,
+ ADD_FOLDER,
+ ADD_NEW_FOLDER
+}
+
class SpaceSetupActivity : BaseActivity() {
companion object {
const val FRAGMENT_TAG = "ssa_fragment"
}
- private lateinit var mBinding: ActivitySpaceSetupBinding
+ private lateinit var binding: ActivitySpaceSetupBinding
+
+ private lateinit var navController: NavController
+ private lateinit var navGraph: NavGraph
+ private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- mBinding = ActivitySpaceSetupBinding.inflate(layoutInflater)
- setContentView(mBinding.root)
+ binding = ActivitySpaceSetupBinding.inflate(layoutInflater)
+ setContentView(binding.root)
setupToolbar(
- title = "Servers",
showBackButton = true
)
- initSpaceSetupFragmentBindings()
- initWebDavFragmentBindings()
- initWebDavCreativeLicenseBindings()
- initSpaceSetupSuccessFragmentBindings()
- initInternetArchiveFragmentBindings()
- initGDriveFragmentBindings()
- initRavenBindings()
+// onBackButtonPressed {
+//
+// if (supportFragmentManager.backStackEntryCount > 1) {
+// // We still have fragments in the back stack to pop
+// supportFragmentManager.popBackStack()
+// true // fully handled here
+// } else {
+// // No more fragments left in back stack, let the system finish Activity
+// false
+// }
+// }
- onBackButtonPressed {
- // Return "true" if you fully handle the back press yourself
- // Return "false" if you want to let the system handle it (i.e., finish the Activity)
- if (supportFragmentManager.backStackEntryCount > 1) {
- // We still have fragments in the back stack to pop
- supportFragmentManager.popBackStack()
- true // fully handled here
- } else {
- // No more fragments left in back stack, let the system finish Activity
- false
- }
- }
+ initSpaceSetupNavigation()
+ }
+
+ private fun initSpaceSetupNavigation() {
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.space_nav_host_fragment) as NavHostFragment
- intent.getBooleanExtra("snowbird", false).let {
- if (it) {
- navigateToFragment(SnowbirdFragment.newInstance())
+ navController = navHostFragment.navController
+ navGraph = navController.navInflater.inflate(R.navigation.space_setup_navigation)
+
+ val startDestinationString =
+ intent.getStringExtra("start_destination") ?: StartDestination.SPACE_TYPE.name
+ val startDestination = StartDestination.valueOf(startDestinationString)
+ when (startDestination) {
+ StartDestination.SPACE_LIST -> {
+ navGraph.setStartDestination(R.id.fragment_space_list)
+ }
+ StartDestination.ADD_FOLDER -> {
+ navGraph.setStartDestination(R.id.fragment_add_folder)
+ }
+ StartDestination.ADD_NEW_FOLDER -> {
+ navGraph.setStartDestination(R.id.fragment_create_new_folder)
+ }
+ else -> {
+ navGraph.setStartDestination(R.id.fragment_space_setup)
}
}
+ navController.graph = navGraph
+
+ appBarConfiguration = AppBarConfiguration(emptySet())
+ setupActionBarWithNavController(navController, appBarConfiguration)
}
fun updateToolbarFromFragment(fragment: Fragment) {
@@ -87,240 +125,7 @@ class SpaceSetupActivity : BaseActivity() {
}
}
- private fun initSpaceSetupSuccessFragmentBindings() {
- supportFragmentManager.setFragmentResultListener(
- SpaceSetupSuccessFragment.RESP_DONE,
- this
- ) { key, bundle ->
- finishAffinity()
- startActivity(Intent(this, MainActivity::class.java))
- }
- }
-
- private fun initSpaceSetupFragmentBindings() {
- supportFragmentManager.setFragmentResultListener(
- SpaceSetupFragment.RESULT_REQUEST_KEY,
- this
- ) { _, bundle ->
- when (bundle.getString(SpaceSetupFragment.RESULT_BUNDLE_KEY)) {
- SpaceSetupFragment.RESULT_VAL_INTERNET_ARCHIVE -> {
- navigateToFragment(InternetArchiveFragment.newInstance())
- }
-
- SpaceSetupFragment.RESULT_VAL_WEBDAV -> {
- navigateToFragment(WebDavFragment.newInstance())
- }
-
- SpaceSetupFragment.RESULT_VAL_GDRIVE -> {
- navigateToFragment(GDriveFragment())
- }
-
- SpaceSetupFragment.RESULT_VAL_RAVEN -> {
- navigateToFragment(SnowbirdFragment.newInstance())
- }
- }
- }
- }
-
- /**
- * Init NextCloud credentials
- *
- */
- private fun initWebDavFragmentBindings() {
- supportFragmentManager.setFragmentResultListener(
- WebDavFragment.RESP_SAVED,
- this
- ) { key, bundle ->
- val spaceId = bundle.getLong(WebDavFragment.ARG_SPACE_ID)
- val fragment =
- WebDavSetupLicenseFragment.newInstance(spaceId = spaceId, isEditing = false)
- navigateToFragment(fragment)
- }
-
-
- supportFragmentManager.setFragmentResultListener(
- WebDavFragment.RESP_CANCEL,
- this
- ) { key, bundle ->
- navigateToFragment(SpaceSetupFragment())
- }
- }
-
- /**
- * Init select Creative Commons Licensing
- *
- */
- private fun initWebDavCreativeLicenseBindings() {
- supportFragmentManager.setFragmentResultListener(
- WebDavSetupLicenseFragment.RESP_SAVED,
- this
- ) { key, bundle ->
- val message = getString(R.string.you_have_successfully_connected_to_a_private_server)
- val fragment = SpaceSetupSuccessFragment.newInstance(message)
- navigateToFragment(fragment)
- }
-
- supportFragmentManager.setFragmentResultListener(
- WebDavSetupLicenseFragment.RESP_CANCEL,
- this
- ) { key, bundle ->
- navigateToFragment(SpaceSetupFragment())
- }
- }
-
- private fun initInternetArchiveFragmentBindings() {
- supportFragmentManager.setFragmentResultListener(
- InternetArchiveFragment.RESP_SAVED,
- this
- ) { key, bundle ->
- val fragment =
- SpaceSetupSuccessFragment.newInstance(getString(R.string.you_have_successfully_connected_to_the_internet_archive))
- navigateToFragment(fragment)
- }
-
- supportFragmentManager.setFragmentResultListener(
- InternetArchiveFragment.RESP_CANCEL,
- this
- ) { key, bundle ->
- navigateToFragment(SpaceSetupFragment())
- }
- }
-
- private fun initGDriveFragmentBindings() {
- supportFragmentManager.setFragmentResultListener(
- GDriveFragment.RESP_CANCEL,
- this
- ) { key, bundle ->
-
- navigateToFragment(SpaceSetupFragment())
- }
-
- supportFragmentManager.setFragmentResultListener(
- GDriveFragment.RESP_AUTHENTICATED,
- this
- ) { key, bundle ->
- val fragment =
- SpaceSetupSuccessFragment.newInstance(getString(R.string.you_have_successfully_connected_to_gdrive))
- navigateToFragment(fragment)
- }
- }
-
- private fun initRavenBindings() {
-
- initSnowbirdFragmentBindings()
-
- initSnowbirdGroupListFragmentBindings()
-
- initSnowbirdCreateGroupFragmentBindings()
-
- initSnowbirdRepoListFragmentBindings()
-
- }
-
- private fun initSnowbirdFragmentBindings() {
- supportFragmentManager.setFragmentResultListener(
- SnowbirdFragment.RESULT_REQUEST_KEY,
- this
- ) { key, bundle ->
- when (bundle.getString(SnowbirdFragment.RESULT_BUNDLE_KEY)) {
-
- SnowbirdFragment.RESULT_VAL_RAVEN_MY_GROUPS -> {
- navigateToFragment(SnowbirdGroupListFragment.newInstance())
- }
-
- SnowbirdFragment.RESULT_VAL_RAVEN_CREATE_GROUP -> {
- val fragment = SnowbirdCreateGroupFragment.newInstance()
- navigateToFragment(fragment)
- }
-
- SnowbirdFragment.RESULT_VAL_RAVEN_JOIN_GROUPS -> {
- val uriString = bundle.getString(SnowbirdFragment.RESULT_VAL_RAVEN_JOIN_GROUPS_ARG) ?: ""
- navigateToFragment(SnowbirdJoinGroupFragment.newInstance(uriString))
- }
- }
- }
- }
-
- private fun initSnowbirdGroupListFragmentBindings() {
- supportFragmentManager.setFragmentResultListener(
- SnowbirdGroupListFragment.RESULT_REQUEST_KEY,
- this
- ) { key, bundle ->
-
- when (bundle.getString(SnowbirdGroupListFragment.RESULT_BUNDLE_NAVIGATION_KEY)) {
- SnowbirdGroupListFragment.RESULT_VAL_RAVEN_CREATE_GROUP_SCREEN -> {
- val fragment = SnowbirdCreateGroupFragment.newInstance()
- navigateToFragment(fragment)
- }
- SnowbirdGroupListFragment.RESULT_VAL_RAVEN_REPO_LIST_SCREEN -> {
- val groupKey = bundle.getString(SnowbirdGroupListFragment.RESULT_BUNDLE_GROUP_KEY) ?: ""
- val fragment = SnowbirdRepoListFragment.newInstance(groupKey)
- navigateToFragment(fragment)
- }
- SnowbirdGroupListFragment.RESULT_VAL_RAVEN_SHARE_SCREEN -> {
- val groupKey = bundle.getString(SnowbirdGroupListFragment.RESULT_BUNDLE_GROUP_KEY) ?: ""
- val fragment = SnowbirdShareFragment.newInstance(groupKey)
- navigateToFragment(fragment)
- }
- }
- }
- }
-
- private fun initSnowbirdCreateGroupFragmentBindings() {
- supportFragmentManager.setFragmentResultListener(
- SnowbirdCreateGroupFragment.RESULT_REQUEST_KEY,
- this
- ) { key, bundle ->
- when(bundle.getString(SnowbirdCreateGroupFragment.RESULT_NAVIGATION_KEY)) {
- SnowbirdCreateGroupFragment.RESULT_NAVIGATION_VAL_SHARE_SCREEN -> {
- val groupKey =
- bundle.getString(SnowbirdCreateGroupFragment.RESULT_BUNDLE_GROUP_KEY) ?: ""
- val fragment = SnowbirdShareFragment.newInstance(groupKey)
- navigateToFragment(fragment)
- }
- }
- }
- }
-
- private fun initSnowbirdRepoListFragmentBindings() {
- supportFragmentManager.setFragmentResultListener(
- SnowbirdRepoListFragment.RESULT_REQUEST_KEY,
- this
- ) { key, bundle ->
- val groupKey = bundle.getString(SnowbirdRepoListFragment.RESULT_VAL_RAVEN_GROUP_KEY) ?: ""
- val repoKey = bundle.getString(SnowbirdRepoListFragment.RESULT_VAL_RAVEN_REPO_KEY) ?: ""
- val fragment = SnowbirdFileListFragment.newInstance(
- groupKey = groupKey,
- repoKey = repoKey
- )
- navigateToFragment(fragment)
- }
- }
-
-
-// @Deprecated("Deprecated in Java")
-// override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-// super.onActivityResult(requestCode, resultCode, data)
-// supportFragmentManager.findFragmentByTag(FRAGMENT_TAG)?.let {
-// onActivityResult(requestCode, resultCode, data)
-// }
-// }
-
- private fun navigateToFragment(
- fragment: BaseFragment,
- addToBackstack: Boolean = true
- ) {
- supportFragmentManager
- .beginTransaction()
- .setCustomAnimations(
- R.anim.slide_in_right,
- R.anim.slide_out_left,
- R.anim.slide_in_left,
- R.anim.slide_out_right
- )
- .replace(mBinding.spaceSetupFragment.id, fragment, FRAGMENT_TAG)
- .apply {
- if (addToBackstack) addToBackStack(null)
- }.commit()
+ override fun onSupportNavigateUp(): Boolean {
+ return findNavController(R.id.space_nav_host_fragment).navigateUp() || super.onSupportNavigateUp()
}
}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/CcSelector.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/CcSelector.kt
deleted file mode 100644
index 175db133..00000000
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/CcSelector.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-package net.opendasharchive.openarchive.features.settings
-
-import net.opendasharchive.openarchive.databinding.ContentCcBinding
-import net.opendasharchive.openarchive.util.extensions.openBrowser
-import net.opendasharchive.openarchive.util.extensions.styleAsLink
-import net.opendasharchive.openarchive.util.extensions.toggle
-
-object CcSelector {
-
- private const val CC_DOMAIN = "creativecommons.org"
- private const val CC_URL = "https://%s/licenses/%s/4.0/"
-
- fun init(cc: ContentCcBinding, license: String? = null, enabled: Boolean = true, update: ((license: String?) -> Unit)? = null) {
- set(cc, license, enabled)
-
- cc.swCc.setOnCheckedChangeListener { _, isChecked ->
- toggle(cc, isChecked)
-
- @Suppress("NAME_SHADOWING")
- val license = get(cc)
-
- update?.invoke(license)
- }
-
- cc.swNd.setOnCheckedChangeListener { _, isChecked ->
- cc.swSa.isEnabled = isChecked
-
- @Suppress("NAME_SHADOWING")
- val license = get(cc)
-
- update?.invoke(license)
- }
-
- cc.swSa.setOnCheckedChangeListener { _, _ ->
- @Suppress("NAME_SHADOWING")
- val license = get(cc)
-
- update?.invoke(license)
- }
- cc.swNc.setOnCheckedChangeListener { _, _ ->
- @Suppress("NAME_SHADOWING")
- val license = get(cc)
-
- update?.invoke(license)
- }
-
- cc.tvLicense.setOnClickListener {
- it?.context?.openBrowser(cc.tvLicense.text.toString())
- }
-
- cc.btLearnMore.styleAsLink()
- cc.btLearnMore.setOnClickListener {
- it?.context?.openBrowser("https://creativecommons.org/about/cclicenses/")
- }
- }
-
- fun set(cc: ContentCcBinding, license: String?, enabled: Boolean = true) {
- val isCc = license?.contains(CC_DOMAIN, true) ?: false
-
- cc.swCc.isChecked = isCc
- toggle(cc, isCc)
-
- cc.swNd.isChecked = isCc && !(license?.contains("-nd", true) ?: false)
- cc.swSa.isEnabled = cc.swNd.isChecked
- cc.swSa.isChecked = isCc && cc.swNd.isChecked && license?.contains("-sa", true) ?: false
- cc.swNc.isChecked = isCc && !(license?.contains("-nc", true) ?: false)
-
- cc.tvLicense.text = license
- cc.tvLicense.styleAsLink()
-
- cc.swCc.isEnabled = enabled
- cc.swNd.isEnabled = enabled
- cc.swSa.isEnabled = enabled
- cc.swNc.isEnabled = enabled
- }
-
- fun get(cc: ContentCcBinding): String? {
- var license: String? = null
-
- if (cc.swCc.isChecked) {
- license = "by"
-
- if (cc.swNd.isChecked) {
- if (!cc.swNc.isChecked) {
- license += "-nc"
- }
-
- if (cc.swSa.isChecked) {
- license += "-sa"
- }
- }
- else {
- cc.swSa.isChecked = false
-
- if (!cc.swNc.isChecked) {
- license += "-nc"
- }
-
- license += "-nd"
- }
- }
-
- if (license != null) {
- license = String.format(CC_URL, CC_DOMAIN, license)
- }
-
- cc.tvLicense.text = license
- cc.tvLicense.styleAsLink()
-
- return license
- }
-
- private fun toggle(cc: ContentCcBinding, value: Boolean) {
- cc.row1.toggle(value)
- cc.row2.toggle(value)
- cc.row3.toggle(value)
- cc.tvLicense.toggle(value)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/CreativeCommonsLicenseManager.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/CreativeCommonsLicenseManager.kt
new file mode 100644
index 00000000..de82223f
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/CreativeCommonsLicenseManager.kt
@@ -0,0 +1,119 @@
+package net.opendasharchive.openarchive.features.settings
+
+import net.opendasharchive.openarchive.databinding.ContentCcBinding
+import net.opendasharchive.openarchive.util.extensions.openBrowser
+import net.opendasharchive.openarchive.util.extensions.styleAsLink
+import net.opendasharchive.openarchive.util.extensions.toggle
+
+object CreativeCommonsLicenseManager {
+
+ private const val CC_DOMAIN = "creativecommons.org"
+ private const val CC_LICENSE_URL_FORMAT = "https://%s/licenses/%s/4.0/"
+
+ fun initialize(
+ binding: ContentCcBinding,
+ currentLicense: String? = null,
+ enabled: Boolean = true,
+ update: ((license: String?) -> Unit)? = null
+ ) {
+ configureInitialState(binding, currentLicense, enabled)
+
+ with(binding) {
+ swCcEnabled.setOnCheckedChangeListener { _, isChecked ->
+ setShowLicenseOptions(binding, isChecked)
+ val license = getSelectedLicenseUrl(binding)
+ update?.invoke(license)
+ }
+
+ swAllowRemix.setOnCheckedChangeListener { _, isChecked ->
+ swRequireShareAlike.isEnabled = isChecked
+ val license = getSelectedLicenseUrl(binding)
+ update?.invoke(license)
+ }
+
+ swRequireShareAlike.setOnCheckedChangeListener { _, _ ->
+ val license = getSelectedLicenseUrl(binding)
+ update?.invoke(license)
+ }
+ swAllowCommercial.setOnCheckedChangeListener { _, _ ->
+ val license = getSelectedLicenseUrl(binding)
+ update?.invoke(license)
+ }
+
+ tvLicenseUrl.setOnClickListener {
+ it?.context?.openBrowser(tvLicenseUrl.text.toString())
+ }
+
+ btLearnMore.styleAsLink()
+ btLearnMore.setOnClickListener {
+ it?.context?.openBrowser("https://creativecommons.org/about/cclicenses/")
+ }
+ }
+ }
+
+ private fun configureInitialState(
+ binding: ContentCcBinding,
+ currentLicense: String?,
+ enabled: Boolean = true
+ ) {
+ val isActive = currentLicense?.contains(CC_DOMAIN, true) ?: false
+
+ with(binding) {
+ swCcEnabled.isChecked = isActive
+ setShowLicenseOptions(this, isActive)
+
+ swAllowRemix.isChecked = isActive && !(currentLicense?.contains("-nd", true) ?: false)
+ swRequireShareAlike.isEnabled = binding.swAllowRemix.isChecked
+ swRequireShareAlike.isChecked = isActive && binding.swAllowRemix.isChecked && currentLicense?.contains("-sa", true) ?: false
+ swAllowCommercial.isChecked = isActive && !(currentLicense?.contains("-nc", true) ?: false)
+ tvLicenseUrl.text = currentLicense
+ tvLicenseUrl.styleAsLink()
+ swCcEnabled.isEnabled = enabled
+ swAllowRemix.isEnabled = enabled
+ swRequireShareAlike.isEnabled = enabled
+ swAllowCommercial.isEnabled = enabled
+ }
+ }
+
+ fun getSelectedLicenseUrl(cc: ContentCcBinding): String? {
+ var license: String? = null
+
+ if (cc.swCcEnabled.isChecked) {
+ license = "by"
+
+ if (cc.swAllowRemix.isChecked) {
+ if (!cc.swAllowCommercial.isChecked) {
+ license += "-nc"
+ }
+
+ if (cc.swRequireShareAlike.isChecked) {
+ license += "-sa"
+ }
+ } else {
+ cc.swRequireShareAlike.isChecked = false
+
+ if (!cc.swAllowCommercial.isChecked) {
+ license += "-nc"
+ }
+
+ license += "-nd"
+ }
+ }
+
+ if (license != null) {
+ license = String.format(CC_LICENSE_URL_FORMAT, CC_DOMAIN, license)
+ }
+
+ cc.tvLicenseUrl.text = license
+ cc.tvLicenseUrl.styleAsLink()
+
+ return license
+ }
+
+ private fun setShowLicenseOptions(binding: ContentCcBinding, isVisible: Boolean) {
+ binding.rowAllowRemix.toggle(isVisible)
+ binding.rowShareAlike.toggle(isVisible)
+ binding.rowCommercialUse.toggle(isVisible)
+ binding.tvLicenseUrl.toggle(isVisible)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderActivity.kt
index 004e7ce3..bffe5653 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderActivity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderActivity.kt
@@ -7,7 +7,10 @@ import net.opendasharchive.openarchive.R
import net.opendasharchive.openarchive.databinding.ActivityEditFolderBinding
import net.opendasharchive.openarchive.db.Project
import net.opendasharchive.openarchive.features.core.BaseActivity
-import net.opendasharchive.openarchive.util.AlertHelper
+import net.opendasharchive.openarchive.features.core.UiImage
+import net.opendasharchive.openarchive.features.core.UiText
+import net.opendasharchive.openarchive.features.core.dialog.DialogType
+import net.opendasharchive.openarchive.features.core.dialog.showDialog
import net.opendasharchive.openarchive.util.extensions.Position
import net.opendasharchive.openarchive.util.extensions.setDrawable
@@ -55,14 +58,14 @@ class EditFolderActivity : BaseActivity() {
mBinding.btRemove.setDrawable(R.drawable.ic_delete, Position.Start, 0.5)
mBinding.btRemove.setOnClickListener {
- removeProject()
+ showDeleteFolderConfirmDialog()
}
mBinding.btArchive.setOnClickListener {
archiveProject()
}
- CcSelector.init(mBinding.cc, null) {
+ CreativeCommonsLicenseManager.initialize(mBinding.cc, null) {
mProject.licenseUrl = it
mProject.save()
}
@@ -70,14 +73,26 @@ class EditFolderActivity : BaseActivity() {
updateUi()
}
- private fun removeProject() {
- AlertHelper.show(this, R.string.action_remove_project, R.string.remove_from_app, buttons = listOf(
- AlertHelper.positiveButton(R.string.remove) { _, _ ->
- mProject.delete()
-
- finish()
- },
- AlertHelper.negativeButton()))
+ private fun showDeleteFolderConfirmDialog() {
+ dialogManager.showDialog(dialogManager.requireResourceProvider()) {
+ type = DialogType.Error
+ icon = UiImage.DrawableResource(R.drawable.ic_trash)
+ title = UiText.StringResource(R.string.remove_from_app)
+ message = UiText.StringResource(R.string.action_remove_project)
+ destructiveButton {
+ text = UiText.StringResource(R.string.remove)
+ action = {
+ mProject.delete()
+ finish()
+ }
+ }
+ neutralButton {
+ text = UiText.StringResource(R.string.lbl_Cancel)
+ action = {
+ dialogManager.dismissDialog()
+ }
+ }
+ }
}
private fun archiveProject() {
@@ -101,10 +116,10 @@ class EditFolderActivity : BaseActivity() {
val global = mProject.space?.license != null
if (global) {
- mBinding.cc.tvCc.setText(R.string.set_the_same_creative_commons_license_for_all_folders_on_this_server)
+ mBinding.cc.tvCcLabel.setText(R.string.set_the_same_creative_commons_license_for_all_folders_on_this_server)
}
- CcSelector.set(mBinding.cc, mProject.licenseUrl, !mProject.isArchived && !global)
+ CreativeCommonsLicenseManager.initialize(mBinding.cc, mProject.licenseUrl, !mProject.isArchived && !global)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/FoldersActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/FoldersActivity.kt
index 35c43879..b1d2c2f4 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/FoldersActivity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/FoldersActivity.kt
@@ -14,6 +14,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.util.extensions.toggle
class FoldersActivity : BaseActivity(), FolderAdapterListener {
@@ -57,9 +58,18 @@ class FoldersActivity : BaseActivity(), FolderAdapterListener {
}
private fun setupButtons() {
- mBinding.fabAdd.apply {
- visibility = if (mArchived) View.INVISIBLE else View.VISIBLE
- setOnClickListener { addFolder() }
+// mBinding.fabAdd.apply {
+// visibility = if (mArchived) View.INVISIBLE else View.VISIBLE
+// setOnClickListener { addFolder() }
+// }
+
+ mBinding.btViewArchived.apply {
+ toggle(!mArchived)
+ setOnClickListener {
+ val i = Intent(this@FoldersActivity, FoldersActivity::class.java)
+ i.putExtra(EXTRA_SHOW_ARCHIVED, true)
+ startActivity(i)
+ }
}
}
@@ -112,23 +122,13 @@ class FoldersActivity : BaseActivity(), FolderAdapterListener {
startActivity(intent)
}
- private fun addFolder() {
- val intent = Intent(this, AddFolderActivity::class.java)
- startActivity(intent)
- }
-
- override fun getSelectedProject(): Project? {
- return Space.current?.projects?.find { it.id == mSelectedProjectId }
- }
override fun projectClicked(project: Project) {
val resultIntent = Intent()
resultIntent.putExtra("SELECTED_FOLDER_ID", project.id)
setResult(RESULT_OK, resultIntent)
finish() // Close FoldersActivity and return to MainActivity
- }
- override fun projectEdit(project: Project) {
val intent = Intent(this, EditFolderActivity::class.java).apply {
putExtra(EditFolderActivity.EXTRA_CURRENT_PROJECT_ID, project.id)
}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeScreen.kt
new file mode 100644
index 00000000..ef471be7
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeScreen.kt
@@ -0,0 +1,199 @@
+package net.opendasharchive.openarchive.features.settings
+
+import android.content.Intent
+import android.net.Uri
+import android.provider.Settings
+import android.text.Spanned
+import android.text.style.URLSpan
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+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.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+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.platform.LocalUriHandler
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.fromHtml
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.text.HtmlCompat
+import me.zhanghai.compose.preference.ProvidePreferenceLocals
+import me.zhanghai.compose.preference.switchPreference
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview
+import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme
+import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ComposeAppBar
+import net.opendasharchive.openarchive.features.settings.passcode.components.DefaultScaffold
+
+@Composable
+fun ProofModeScreen(
+ onNavigateBack: () -> Unit
+) {
+
+ SaveAppTheme {
+
+
+ DefaultScaffold(
+ topAppBar = {
+ ComposeAppBar(
+ title = stringResource(R.string.proofmode),
+ onNavigationAction = {
+ onNavigateBack()
+ }
+ )
+ },
+
+ ) {
+
+ ProofModeScreenContent()
+ }
+ }
+}
+
+@Composable
+fun ProofModeScreenContent() {
+ val context = LocalContext.current
+ val uriHandler = LocalUriHandler.current
+
+
+ val permissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
+ if (!isGranted) {
+ Toast.makeText(context, "Please allow all permissions", Toast.LENGTH_LONG).show()
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ val uri = Uri.fromParts("package", context.packageName, null)
+ intent.data = uri
+ context.startActivity(intent)
+ }
+ }
+
+ val useProofModeKeyEncryption = remember { mutableStateOf(false) }
+
+ val spannedText: Spanned = HtmlCompat.fromHtml(
+ stringResource(
+ R.string.prefs_use_proofmode_description,
+ "https://www.google.com"
+ ), HtmlCompat.FROM_HTML_MODE_COMPACT
+ )
+
+ // AnnotatedString Builder
+ val annotatedString = buildAnnotatedString {
+ append(spannedText.toString())
+ spannedText.getSpans(0, spannedText.length, URLSpan::class.java)
+ .forEach { urlSpan ->
+ val start = spannedText.getSpanStart(urlSpan)
+ val end = spannedText.getSpanEnd(urlSpan)
+ addStringAnnotation(
+ tag = "URL",
+ annotation = urlSpan.url,
+ start = start,
+ end = end
+ )
+ addStyle(
+ style = SpanStyle(
+ color = MaterialTheme.colorScheme.tertiary,
+ textDecoration = TextDecoration.Underline
+ ),
+ start = start,
+ end = end
+ )
+ }
+ }
+
+ ProvidePreferenceLocals {
+ val useProofModeKey = stringResource(R.string.pref_key_use_proof_mode)
+
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+
+
+ switchPreference(
+ key = useProofModeKey,
+ defaultValue = false,
+ enabled = {
+ true
+ },
+ rememberState = {
+ useProofModeKeyEncryption
+ },
+ title = { Text(stringResource(R.string.prefs_use_proofmode_title)) },
+ summary = { Text(stringResource(R.string.prefs_use_proofmode_summary)) }
+ )
+
+ item {
+ Box(modifier = Modifier.padding(horizontal = 16.dp)) {
+ Text(annotatedString, fontSize = 11.sp)
+ }
+ }
+
+ item {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 24.dp)
+ ) {
+
+ Card(
+ shape = RoundedCornerShape(8.dp)
+ ) {
+
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.Top,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ Icons.Outlined.Info,
+ tint = MaterialTheme.colorScheme.error,
+ contentDescription = null
+ )
+ Text(
+ text = AnnotatedString.fromHtml(
+ stringResource(R.string.proof_mode_warning_text),
+ linkStyles = TextLinkStyles(
+ style = SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ fontStyle = FontStyle.Italic,
+ color = Color.Blue
+ )
+ )
+ ),
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun ProofModeScreenPreview() {
+ DefaultScaffoldPreview {
+ ProofModeScreenContent()
+ }
+}
\ No newline at end of file
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 c79b7aac..f75bbf11 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
@@ -8,10 +8,13 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
+import android.text.Spanned
+import android.text.method.LinkMovementMethod
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
+import androidx.core.text.HtmlCompat
import androidx.fragment.app.FragmentActivity
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
@@ -31,17 +34,18 @@ import java.io.IOException
import java.util.UUID
import javax.crypto.SecretKey
-class ProofModeSettingsActivity: BaseActivity() {
+class ProofModeSettingsActivity : BaseActivity() {
- class Fragment: PreferenceFragmentCompat() {
+ class Fragment : PreferenceFragmentCompat() {
- private val enrollBiometrics = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- findPreference(Prefs.USE_PROOFMODE_KEY_ENCRYPTION)?.let {
- MainScope().launch {
- enableProofModeKeyEncryption(it)
+ 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)
@@ -61,13 +65,7 @@ class ProofModeSettingsActivity: BaseActivity() {
proofModeSwitch?.isChecked = Prefs.getBoolean(Prefs.USE_PROOFMODE, false)
}
- findPreference("share_proofmode")?.onPreferenceClickListener =
- Preference.OnPreferenceClickListener {
- shareKey(requireActivity())
- true
- }
-
- findPreference(Prefs.USE_PROOFMODE)?.setOnPreferenceChangeListener { preference, newValue ->
+ getPrefByKey(R.string.pref_key_use_proof_mode)?.setOnPreferenceChangeListener { preference, newValue ->
if (newValue as Boolean) {
PermissionX.init(this)
.permissions(Manifest.permission.READ_PHONE_STATE)
@@ -80,11 +78,17 @@ class ProofModeSettingsActivity: BaseActivity() {
.request { allGranted, _, _ ->
if (!allGranted) {
(preference as? SwitchPreferenceCompat)?.isChecked = false
- Toast.makeText(activity,"Please allow all permissions", Toast.LENGTH_LONG).show()
+ Toast.makeText(
+ activity,
+ "Please allow all permissions",
+ Toast.LENGTH_LONG
+ ).show()
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", activity?.packageName, null)
intent.data = uri
activity?.startActivity(intent)
+ } else {
+ (preference as? SwitchPreferenceCompat)?.isChecked = true
}
}
}
@@ -92,20 +96,23 @@ class ProofModeSettingsActivity: BaseActivity() {
true
}
- val pkePreference = findPreference(Prefs.USE_PROOFMODE_KEY_ENCRYPTION)
+ val pkePreference =
+ findPreference(Prefs.USE_PROOFMODE_KEY_ENCRYPTION)
val activity = activity
val availability = Hbks.deviceAvailablity(requireContext())
if (activity != null && availability !is Hbks.Availability.Unavailable) {
pkePreference?.isSingleLineTitle = false
- pkePreference?.setTitle(when (Hbks.biometryType(activity)) {
- Hbks.BiometryType.StrongBiometry -> R.string.prefs_proofmode_key_encryption_title_biometrics
+ pkePreference?.setTitle(
+ when (Hbks.biometryType(activity)) {
+ Hbks.BiometryType.StrongBiometry -> R.string.prefs_proofmode_key_encryption_title_biometrics
- Hbks.BiometryType.DeviceCredential -> R.string.prefs_proofmode_key_encryption_title_passcode
+ Hbks.BiometryType.DeviceCredential -> R.string.prefs_proofmode_key_encryption_title_passcode
- else -> R.string.prefs_proofmode_key_encryption_title_all
- })
+ else -> R.string.prefs_proofmode_key_encryption_title_all
+ }
+ )
pkePreference?.setOnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
@@ -114,8 +121,7 @@ class ProofModeSettingsActivity: BaseActivity() {
} else {
enableProofModeKeyEncryption(pkePreference)
}
- }
- else {
+ } else {
if (Prefs.proofModeEncryptedPassphrase != null) {
Prefs.proofModeEncryptedPassphrase = null
@@ -127,8 +133,7 @@ class ProofModeSettingsActivity: BaseActivity() {
true
}
- }
- else {
+ } else {
pkePreference?.isVisible = false
}
}
@@ -156,6 +161,11 @@ class ProofModeSettingsActivity: BaseActivity() {
// What?? shouldn't happen if enrolled with a PIN or Fingerprint
}
}
+
+
+ private fun getPrefByKey(key: Int): T? {
+ return findPreference(getString(key))
+ }
}
private lateinit var mBinding: ActivitySettingsContainerBinding
@@ -173,6 +183,24 @@ class ProofModeSettingsActivity: BaseActivity() {
.beginTransaction()
.replace(mBinding.container.id, Fragment())
.commit()
+
+// setContent {
+
+// }
+
+
+ val learnModeInfo =
+ getString(R.string.prefs_use_proofmode_description, "https://www.google.com")
+
+
+ val spannedText: Spanned =
+ HtmlCompat.fromHtml(learnModeInfo, HtmlCompat.FROM_HTML_MODE_COMPACT)
+
+ mBinding.proofModeLearnMode.text = spannedText
+
+ mBinding.proofModeLearnMode.movementMethod =
+ LinkMovementMethod.getInstance() // Enable link clicks
+
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -197,13 +225,16 @@ class ProofModeSettingsActivity: BaseActivity() {
intent.putExtra(Intent.EXTRA_TEXT, pubKey)
activity.startActivity(intent)
}
- }
- catch (ioe: IOException) {
+ } catch (ioe: IOException) {
Timber.d("error publishing key")
}
}
- private fun createPassphrase(key: SecretKey, activity: FragmentActivity?, completed: (passphrase: String?) -> Unit) {
+ private fun createPassphrase(
+ key: SecretKey,
+ activity: FragmentActivity?,
+ completed: (passphrase: String?) -> Unit
+ ) {
val passphrase = UUID.randomUUID().toString()
Hbks.encrypt(passphrase, key, activity) { ciphertext, _ ->
@@ -213,7 +244,11 @@ class ProofModeSettingsActivity: BaseActivity() {
Prefs.proofModeEncryptedPassphrase = ciphertext
- Hbks.decrypt(Prefs.proofModeEncryptedPassphrase, key, activity) { decrpytedPassphrase, _ ->
+ Hbks.decrypt(
+ Prefs.proofModeEncryptedPassphrase,
+ key,
+ activity
+ ) { decrpytedPassphrase, _ ->
if (decrpytedPassphrase == null || decrpytedPassphrase != passphrase) {
Prefs.proofModeEncryptedPassphrase = null
@@ -225,4 +260,6 @@ class ProofModeSettingsActivity: BaseActivity() {
}
}
}
-}
\ No newline at end of file
+}
+
+
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 f9c600eb..3ca6296f 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt
@@ -1,7 +1,6 @@
package net.opendasharchive.openarchive.features.settings
import android.app.Activity
-import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.view.View
@@ -11,18 +10,26 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import net.opendasharchive.openarchive.R
import net.opendasharchive.openarchive.features.core.BaseActivity
+import net.opendasharchive.openarchive.features.core.UiText
+import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager
+import net.opendasharchive.openarchive.features.core.dialog.DialogType
+import net.opendasharchive.openarchive.features.core.dialog.showDialog
+import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity
+import net.opendasharchive.openarchive.features.onboarding.StartDestination
import net.opendasharchive.openarchive.features.settings.passcode.PasscodeRepository
import net.opendasharchive.openarchive.features.settings.passcode.passcode_setup.PasscodeSetupActivity
-import net.opendasharchive.openarchive.features.spaces.SpacesActivity
import net.opendasharchive.openarchive.util.Prefs
import net.opendasharchive.openarchive.util.Theme
import net.opendasharchive.openarchive.util.extensions.getVersionName
import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.activityViewModel
class SettingsFragment : PreferenceFragmentCompat() {
private val passcodeRepository by inject()
+ private val dialogManager: DialogStateManager by activityViewModel()
+
private var passcodePreference: SwitchPreferenceCompat? = null
@@ -71,20 +78,26 @@ class SettingsFragment : PreferenceFragmentCompat() {
activityResultLauncher.launch(intent)
} else {
// Show confirmation dialog
- AlertDialog.Builder(requireContext())
- .setTitle("Disable Passcode")
- .setMessage("Are you sure you want to disable the passcode?")
- .setPositiveButton("Yes") { _, _ ->
- passcodeRepository.clearPasscode()
- passcodePreference?.isChecked = false
-
- // Update the FLAG_SECURE dynamically
- (activity as? BaseActivity)?.updateScreenshotPrevention()
+ dialogManager.showDialog(dialogManager.requireResourceProvider()) {
+ type = DialogType.Warning
+ title = UiText.StringResource(R.string.disable_passcode_dialog_title)
+ message = UiText.StringResource(R.string.disable_passcode_dialog_msg)
+ positiveButton {
+ text = UiText.StringResource(R.string.answer_yes)
+ action = {
+ passcodeRepository.clearPasscode()
+ passcodePreference?.isChecked = false
+
+ // Update the FLAG_SECURE dynamically
+ (activity as? BaseActivity)?.updateScreenshotPrevention()
+ }
}
- .setNegativeButton("No") { _, _ ->
- passcodePreference?.isChecked = true
+ neutralButton {
+ action = {
+ passcodePreference?.isChecked = true
+ }
}
- .show()
+ }
}
// Return false to avoid the preference updating immediately
false
@@ -101,7 +114,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
getPrefByKey(R.string.pref_media_servers)?.setOnPreferenceClickListener {
- startActivity(Intent(context, SpacesActivity::class.java))
+ val intent = Intent(context, SpaceSetupActivity::class.java)
+ intent.putExtra("start_destination", StartDestination.SPACE_LIST.name)
+ startActivity(intent)
true
}
@@ -110,20 +125,39 @@ class SettingsFragment : PreferenceFragmentCompat() {
true
}
- findPreference("proof_mode")?.setOnPreferenceClickListener {
+ getPrefByKey(R.string.pref_key_proof_mode)?.setOnPreferenceClickListener {
startActivity(Intent(context, ProofModeSettingsActivity::class.java))
true
}
findPreference(Prefs.USE_TOR)?.setOnPreferenceChangeListener { _, newValue ->
- //Prefs.useTor = (newValue as Boolean)
+ Prefs.useTor = (newValue as Boolean)
//torViewModel.updateTorServiceState()
- false
+ true
}
+ getPrefByKey(R.string.pref_key_use_tor)?.isEnabled = false
+
findPreference(Prefs.THEME)?.setOnPreferenceChangeListener { _, newValue ->
Theme.set(Theme.get(newValue as? String))
+ true
+ }
+
+ // Retrieve the switch preference
+ val darkModeSwitch = getPrefByKey(R.string.pref_key_use_dark_mode)
+
+ // Get the saved dark mode preference
+ val isDarkModeEnabled = Prefs.getBoolean(getString(R.string.pref_key_use_dark_mode), false)
+
+ // Set the switch state based on the saved preference
+ darkModeSwitch?.isChecked = isDarkModeEnabled
+ getPrefByKey(R.string.pref_key_use_dark_mode)?.setOnPreferenceChangeListener { pref, newValue ->
+ val useDarkMode = newValue as Boolean
+ val theme = if (useDarkMode) Theme.DARK else Theme.LIGHT
+ Theme.set(theme)
+ // Save the preference
+ Prefs.putBoolean(getString(R.string.pref_key_use_dark_mode), useDarkMode)
true
}
@@ -138,7 +172,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
val packageManager = requireActivity().packageManager
val versionText = packageManager.getVersionName(requireActivity().packageName)
- findPreference("app_version")?.summary = versionText
+ getPrefByKey(R.string.pref_key_app_version)?.summary = versionText
}
private fun getPrefByKey(key: Int): T? {
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsScreen.kt
index c0f404a1..aa2002d3 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsScreen.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsScreen.kt
@@ -10,6 +10,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.rememberNavController
+import kotlinx.serialization.Serializable
import me.zhanghai.compose.preference.ProvidePreferenceLocals
import me.zhanghai.compose.preference.listPreference
import me.zhanghai.compose.preference.preference
@@ -18,7 +21,9 @@ import me.zhanghai.compose.preference.switchPreference
import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview
@Composable
-fun SettingsScreen() {
+fun SettingsScreen(
+ onNavigateToCache: () -> Unit = {}
+) {
val context = LocalContext.current
@@ -43,6 +48,14 @@ fun SettingsScreen() {
key = "pref_media_folders",
title = { Text("Media Folders") },
summary = { Text("Add or remove media folders") })
+ preference(
+ key = "pref_media_cache",
+ title = { Text("Media Cache") },
+ summary = { Text("View media cache") },
+ onClick = {
+ onNavigateToCache()
+ }
+ )
// Verify Category
preferenceCategory(title = { Text("Verify") }, key = "verify")
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt
index e5ac7b06..b8bc89bf 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt
@@ -6,10 +6,16 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
+import androidx.fragment.compose.content
+import androidx.navigation.NavDirections
+import androidx.navigation.fragment.findNavController
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme
import net.opendasharchive.openarchive.databinding.FragmentSpaceSetupBinding
import net.opendasharchive.openarchive.db.Space
import net.opendasharchive.openarchive.features.core.BaseFragment
import net.opendasharchive.openarchive.features.settings.passcode.AppConfig
+import net.opendasharchive.openarchive.features.spaces.SpaceSetupScreen
import net.opendasharchive.openarchive.util.extensions.hide
import net.opendasharchive.openarchive.util.extensions.show
import org.koin.android.ext.android.inject
@@ -19,41 +25,62 @@ class SpaceSetupFragment : BaseFragment() {
private val appConfig by inject()
- private lateinit var binding: FragmentSpaceSetupBinding
-
override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?,
+ inflater: LayoutInflater,
+ container: ViewGroup?,
savedInstanceState: Bundle?
- ): View {
- binding = FragmentSpaceSetupBinding.inflate(inflater)
+ ): View = content {
- binding.webdav.setOnClickListener {
- setFragmentResult(RESULT_REQUEST_KEY, bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_WEBDAV))
+ // Prepare click lambdas that use the fragment’s business logic.
+ val onWebDavClick = {
+ if (isJetpackNavigation) {
+ findNavController().navigate(R.id.action_fragment_space_setup_to_fragment_web_dav)
+ } else {
+ setFragmentResult(
+ RESULT_REQUEST_KEY,
+ bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_WEBDAV)
+ )
+ }
}
-
- if (Space.has(Space.Type.INTERNET_ARCHIVE)) {
- this@SpaceSetupFragment.binding.internetArchive.hide()
- } else {
- binding.internetArchive.setOnClickListener {
+ // Only enable Internet Archive if not already present
+ val isInternetArchiveAllowed = !Space.has(Space.Type.INTERNET_ARCHIVE)
+ val onInternetArchiveClick = {
+ if (isJetpackNavigation) {
+ val action =
+ SpaceSetupFragmentDirections.actionFragmentSpaceSetupToFragmentInternetArchive()
+ findNavController().navigate(action)
+ } else {
setFragmentResult(
RESULT_REQUEST_KEY,
bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_INTERNET_ARCHIVE)
)
}
}
-
- if (appConfig.snowbirdEnabled) {
- binding.snowbird.show()
- } else {
- binding.snowbird.hide()
+ // Show/hide Snowbird based on config
+ val isDwebEnabled = appConfig.isDwebEnabled
+ val onDwebClicked = {
+ if (isJetpackNavigation) {
+ val action =
+ SpaceSetupFragmentDirections.actionFragmentSpaceSetupToFragmentSnowbird()
+ findNavController().navigate(action)
+ } else {
+ setFragmentResult(
+ RESULT_REQUEST_KEY,
+ bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_RAVEN)
+ )
+ }
}
-
- binding.snowbird.setOnClickListener {
- setFragmentResult(RESULT_REQUEST_KEY, bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_RAVEN))
+ SaveAppTheme {
+ SpaceSetupScreen(
+ onWebDavClick = onWebDavClick,
+ isInternetArchiveAllowed = isInternetArchiveAllowed,
+ onInternetArchiveClick = onInternetArchiveClick,
+ isDwebEnabled = isDwebEnabled,
+ onDwebClicked = onDwebClicked
+ )
}
- return binding.root
}
companion object {
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupSuccessFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupSuccessFragment.kt
index f4fc75cf..1e637a51 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupSuccessFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupSuccessFragment.kt
@@ -1,5 +1,6 @@
package net.opendasharchive.openarchive.features.settings
+import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -8,8 +9,9 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import net.opendasharchive.openarchive.databinding.FragmentSpaceSetupSuccessBinding
import net.opendasharchive.openarchive.features.core.BaseFragment
+import net.opendasharchive.openarchive.features.main.MainActivity
-class SpaceSetupSuccessFragment private constructor(): BaseFragment() {
+class SpaceSetupSuccessFragment : BaseFragment() {
private lateinit var mBinding: FragmentSpaceSetupSuccessBinding
private var message = ""
@@ -31,7 +33,14 @@ class SpaceSetupSuccessFragment private constructor(): BaseFragment() {
}
mBinding.btAuthenticate.setOnClickListener { _ ->
- setFragmentResult(RESP_DONE, bundleOf())
+ if (isJetpackNavigation) {
+ val intent = Intent(requireActivity(), MainActivity::class.java)
+ intent.flags =
+ Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK // Clears backstack
+ startActivity(intent)
+ } else {
+ setFragmentResult(RESP_DONE, bundleOf())
+ }
}
return mBinding.root
@@ -40,7 +49,7 @@ class SpaceSetupSuccessFragment private constructor(): BaseFragment() {
companion object {
const val RESP_DONE = "space_setup_success_fragment_resp_done"
- const val ARG_MESSAGE = "space_setup_success_fragment_arg_message"
+ const val ARG_MESSAGE = "message"
@JvmStatic
fun newInstance(message: String) =
@@ -51,6 +60,6 @@ class SpaceSetupSuccessFragment private constructor(): BaseFragment() {
}
}
- override fun getToolbarTitle() = "Setup complete"
+ override fun getToolbarTitle() = "Setup Complete"
override fun shouldShowBackButton() = false
}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/AppConfig.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/AppConfig.kt
index d837f93b..69e815b2 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/AppConfig.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/AppConfig.kt
@@ -6,5 +6,6 @@ data class AppConfig(
val maxRetryLimitEnabled: Boolean = false,
val biometricAuthEnabled: Boolean = false,
val maxFailedAttempts: Int = 5,
- val snowbirdEnabled: Boolean = false
+ val isDwebEnabled: Boolean = true,
+ val multipleProjectSelectionMode: Boolean = false
)
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/DefaultScaffold.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/DefaultScaffold.kt
index 5b379134..827ca7b9 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/DefaultScaffold.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/DefaultScaffold.kt
@@ -26,6 +26,7 @@ object MessageManager {
@Composable
fun DefaultScaffold(
modifier: Modifier = Modifier,
+ topAppBar: (@Composable () -> Unit)? = null,
content: @Composable () -> Unit
) {
@@ -39,6 +40,9 @@ fun DefaultScaffold(
Scaffold(
modifier = modifier.fillMaxSize(),
+ topBar = {
+ topAppBar?.invoke()
+ },
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/NumericKeypad.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/NumericKeypad.kt
index f4bd63bc..a371e368 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/NumericKeypad.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/components/NumericKeypad.kt
@@ -19,6 +19,10 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowForward
+import androidx.compose.material.icons.filled.Backspace
+import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@@ -28,11 +32,14 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
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.presentation.theme.SaveAppTheme
import net.opendasharchive.openarchive.features.settings.passcode.AppHapticFeedbackType
import net.opendasharchive.openarchive.features.settings.passcode.HapticManager
@@ -42,13 +49,15 @@ private val keys = listOf(
"1", "2", "3",
"4", "5", "6",
"7", "8", "9",
- "", "0"
+ "delete", "0", "submit"
)
@Composable
fun NumericKeypad(
isEnabled: Boolean = true,
onNumberClick: (String) -> Unit,
+ onDeleteClick: () -> Unit,
+ onSubmitClick: () -> Unit
) {
Box(
@@ -73,7 +82,11 @@ fun NumericKeypad(
label = label,
enabled = isEnabled,
onClick = {
- onNumberClick(label)
+ when (label) {
+ "delete" -> onDeleteClick()
+ "submit" -> onSubmitClick()
+ else -> onNumberClick(label)
+ }
}
)
} else {
@@ -108,7 +121,9 @@ private fun NumericKeypadPreview() {
isEnabled = true,
onNumberClick = { number ->
- }
+ },
+ onDeleteClick = {},
+ onSubmitClick = {}
)
Spacer(modifier = Modifier.height(16.dp))
@@ -125,13 +140,44 @@ private fun NumberButton(
label: String,
enabled: Boolean = true,
onClick: () -> Unit,
- hapticManager: HapticManager = koinInject()
+ //hapticManager: HapticManager = koinInject()
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
+
+ // Determine background color based on button type and pressed state
val backgroundColor by animateColorAsState(
- targetValue = if (isPressed) MaterialTheme.colorScheme.primary.copy(alpha = 0.5f) else Color.Transparent,
+ targetValue = when {
+ isPressed -> when (label) {
+ "delete" -> colorResource(R.color.red_bg).copy(alpha = 0.5f)
+ "submit" -> MaterialTheme.colorScheme.tertiary.copy(alpha = 0.7f)
+ else -> MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
+ }
+ else -> when (label) {
+ "delete" -> colorResource(R.color.red_bg).copy(alpha = 0.3f)
+ "submit" -> MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f)
+ else -> Color.Transparent
+ }
+ },
+ animationSpec = spring(),
+ label = ""
+ )
+
+ // Determine background color based on button type and pressed state
+ val borderColor by animateColorAsState(
+ targetValue = when {
+ isPressed -> when (label) {
+ "delete" -> Color.Transparent
+ "submit" -> Color.Transparent
+ else -> MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f)
+ }
+ else -> when (label) {
+ "delete" -> Color.Transparent
+ "submit" -> Color.Transparent
+ else -> MaterialTheme.colorScheme.tertiary
+ }
+ },
animationSpec = spring(),
label = ""
)
@@ -144,21 +190,34 @@ private fun NumberButton(
indication = null,
enabled = enabled,
onClick = {
- hapticManager.performHapticFeedback(AppHapticFeedbackType.KeyPress)
+ //hapticManager.performHapticFeedback(AppHapticFeedbackType.KeyPress)
onClick()
}
)
- .border(width = 2.dp, color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f), shape = CircleShape)
+ .border(width = 2.dp, color = borderColor, shape = CircleShape)
.size(72.dp),
contentAlignment = Alignment.Center
) {
- Text(
- text = label,
- style = TextStyle(
- fontSize = 24.sp,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onBackground
+
+ when (label) {
+ "delete" -> Icon(
+ imageVector = Icons.Default.Backspace,
+ contentDescription = "Delete",
+ tint = MaterialTheme.colorScheme.onBackground
)
- )
+ "submit" -> Icon(
+ painter = painterResource(R.drawable.ic_arrow_submit),
+ contentDescription = "Submit",
+ tint = MaterialTheme.colorScheme.onBackground
+ )
+ else -> Text(
+ text = label,
+ style = TextStyle(
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onBackground
+ )
+ )
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryScreen.kt
index 38b01d7e..242b050a 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryScreen.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryScreen.kt
@@ -31,6 +31,7 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.collectLatest
import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.presentation.theme.DefaultEmptyScaffoldPreview
import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme
import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview
import net.opendasharchive.openarchive.features.settings.passcode.AppHapticFeedbackType
@@ -160,52 +161,56 @@ fun PasscodeEntryScreenContent(
isEnabled = !state.isProcessing,
onNumberClick = { number ->
onAction(PasscodeEntryScreenAction.OnNumberClick(number))
+ },
+ onDeleteClick = {
+ onAction(PasscodeEntryScreenAction.OnBackspaceClick)
+ },
+ onSubmitClick = {
+
}
)
Spacer(modifier = Modifier.height(16.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceAround
- ) {
- TextButton(
- onClick = {
- onExit()
- }
- ) {
- Text(
- text = "Exit",
- modifier = Modifier.padding(8.dp),
- style = TextStyle(
- fontSize = 16.sp,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onBackground
- ),
- )
- }
-
- TextButton(
- enabled = state.passcode.isNotEmpty(),
- onClick = {
- onAction(PasscodeEntryScreenAction.OnBackspaceClick)
- }
- ) {
- Text(
- text = "Delete",
- modifier = Modifier.padding(8.dp),
- style = TextStyle(
- fontSize = 16.sp,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onBackground
- ),
- )
- }
-
-
- }
-
+ Spacer(modifier = Modifier.height(16.dp))
+// Row(
+// modifier = Modifier.fillMaxWidth(),
+// horizontalArrangement = Arrangement.SpaceAround
+// ) {
+// TextButton(
+// onClick = {
+// onExit()
+// }
+// ) {
+// Text(
+// text = "Exit",
+// modifier = Modifier.padding(8.dp),
+// style = TextStyle(
+// fontSize = 16.sp,
+// fontWeight = FontWeight.Bold,
+// color = MaterialTheme.colorScheme.onBackground
+// ),
+// )
+// }
+//
+// TextButton(
+// enabled = state.passcode.isNotEmpty(),
+// onClick = {
+// onAction(PasscodeEntryScreenAction.OnBackspaceClick)
+// }
+// ) {
+// Text(
+// text = "Delete",
+// modifier = Modifier.padding(8.dp),
+// style = TextStyle(
+// fontSize = 16.sp,
+// fontWeight = FontWeight.Bold,
+// color = MaterialTheme.colorScheme.onBackground
+// ),
+// )
+// }
+// }
}
}
}
@@ -216,15 +221,15 @@ fun PasscodeEntryScreenContent(
@Composable
private fun PasscodeEntryScreenPreview() {
- DefaultScaffoldPreview {
- SaveAppTheme {
- PasscodeEntryScreenContent(
- state = PasscodeEntryScreenState(
- passcodeLength = 6
- ),
- onAction = {},
- onExit = {},
- )
- }
+ DefaultEmptyScaffoldPreview {
+
+ PasscodeEntryScreenContent(
+ state = PasscodeEntryScreenState(
+ passcodeLength = 6
+ ),
+ onAction = {},
+ onExit = {},
+ )
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryViewModel.kt
index 1f9f8b57..0cb3f2a7 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryViewModel.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_entry/PasscodeEntryViewModel.kt
@@ -38,6 +38,7 @@ class PasscodeEntryViewModel(
when (action) {
is PasscodeEntryScreenAction.OnNumberClick -> onNumberClick(action.number)
PasscodeEntryScreenAction.OnBackspaceClick -> onBackspaceClick()
+ PasscodeEntryScreenAction.OnSubmit -> onSubmit()
}
}
@@ -71,6 +72,10 @@ class PasscodeEntryViewModel(
}
}
+ private fun onSubmit() {
+
+ }
+
private fun checkPasscode() = viewModelScope.launch {
val currentState = uiState.value
val currentPasscode = currentState.passcode
@@ -126,6 +131,7 @@ data class PasscodeEntryScreenState(
sealed class PasscodeEntryScreenAction {
data class OnNumberClick(val number: String) : PasscodeEntryScreenAction()
data object OnBackspaceClick : PasscodeEntryScreenAction()
+ data object OnSubmit: PasscodeEntryScreenAction()
}
sealed class PasscodeEntryUiEvent {
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupActivity.kt
index c36ca919..62257065 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupActivity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupActivity.kt
@@ -8,6 +8,7 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme
import net.opendasharchive.openarchive.features.core.BaseActivity
+import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ComposeAppBar
import net.opendasharchive.openarchive.features.settings.passcode.components.DefaultScaffold
class PasscodeSetupActivity : BaseActivity() {
@@ -16,22 +17,31 @@ class PasscodeSetupActivity : BaseActivity() {
const val EXTRA_PASSCODE_ENABLED = "passcode_enabled"
}
- private val onBackPressedCallback = object : OnBackPressedCallback(enabled = true) {
- override fun handleOnBackPressed() {
- setResult(RESULT_CANCELED)
- finish()
- }
- }
+// private val onBackPressedCallback = object : OnBackPressedCallback(enabled = true) {
+// override fun handleOnBackPressed() {
+// setResult(RESULT_CANCELED)
+// finish()
+// }
+// }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- onBackPressedDispatcher.addCallback(onBackPressedCallback)
+ //onBackPressedDispatcher.addCallback(onBackPressedCallback)
setContent {
SaveAppTheme {
- DefaultScaffold {
+ DefaultScaffold(
+ topAppBar = {
+ ComposeAppBar(
+ title = "Lock app with passcode",
+ onNavigationAction = {
+ //onBackPressedCallback.handleOnBackPressed()
+ }
+ )
+ }
+ ) {
PasscodeSetupScreen(
onPasscodeSet = {
// Passcode successfully set
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupScreen.kt
index 229de783..42871f47 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupScreen.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupScreen.kt
@@ -64,7 +64,7 @@ fun PasscodeSetupScreen(
PasscodeSetupUiEvent.PasscodeSet -> onPasscodeSet()
PasscodeSetupUiEvent.PasscodeDoNotMatch -> {
hapticManager.performHapticFeedback(AppHapticFeedbackType.Error)
- MessageManager.showMessage("Passcodes do not match. Try again.")
+ MessageManager.showMessage("Passcodes do not match. Try againss.")
}
PasscodeSetupUiEvent.PasscodeCancelled -> onCancel()
@@ -102,17 +102,19 @@ private fun PasscodeSetupScreenContent(
.padding(horizontal = 24.dp)
.padding(bottom = 24.dp)
) {
- Image(
- painter = painterResource(R.drawable.savelogo),
- contentDescription = null,
- modifier = Modifier.size(100.dp),
- contentScale = ContentScale.Fit
+ Text(
+ text = if (state.isConfirming) "Confirm Passcode" else "Set Passcode",
+ style = TextStyle(
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onBackground
+ )
)
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = "Remember this PIN. If you forget it, you will need to reset the application and all data will be erased.",
+ text = "Make sure you remember this pin. If you forget it, you will need to reset the app, and all data will be erased.",
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Light,
@@ -120,6 +122,16 @@ private fun PasscodeSetupScreenContent(
)
}
+ Spacer(modifier = Modifier.height(32.dp))
+
+ // Passcode dots display
+ PasscodeDots(
+ passcodeLength = state.passcodeLength,
+ currentPasscodeLength = state.passcode.length,
+ shouldShake = state.shouldShake
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
// Middle section with prompt and passcode dots
Column(
@@ -129,76 +141,61 @@ private fun PasscodeSetupScreenContent(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
- Text(
- text = if (state.isConfirming) "Confirm Your Passcode" else "Set Your Passcode",
- style = TextStyle(
- fontSize = 18.sp,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onBackground
- )
- )
-
-
- Spacer(modifier = Modifier.height(32.dp))
-
- // Passcode dots display
- PasscodeDots(
- passcodeLength = state.passcodeLength,
- currentPasscodeLength = state.passcode.length,
- shouldShake = state.shouldShake
- )
-
- Spacer(modifier = Modifier.height(32.dp))
// Custom numeric keypad
NumericKeypad(
isEnabled = !state.isProcessing,
onNumberClick = { number ->
onAction(PasscodeSetupUiAction.OnNumberClick(number))
+ },
+ onDeleteClick = {
+ onAction(PasscodeSetupUiAction.OnBackspaceClick)
+ },
+ onSubmitClick = {
+ onAction(PasscodeSetupUiAction.OnSubmit)
}
)
- Spacer(modifier = Modifier.height(16.dp))
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceAround
- ) {
- TextButton(
- onClick = {
- onAction(PasscodeSetupUiAction.OnCancel)
- }
- ) {
- Text(
- text = "Cancel",
- modifier = Modifier.padding(8.dp),
- style = TextStyle(
- fontSize = 16.sp,
- fontWeight = FontWeight.Bold,
- ),
- )
- }
-
- TextButton(
- enabled = state.passcode.isNotEmpty(),
- onClick = {
- onAction(PasscodeSetupUiAction.OnBackspaceClick)
- }
- ) {
- Text(
- text = "Delete",
- modifier = Modifier.padding(8.dp),
- style = TextStyle(
- fontSize = 16.sp,
- fontWeight = FontWeight.Bold
- ),
- )
- }
-
-
- }
+ Spacer(modifier = Modifier.height(64.dp))
+
+
+// Row(
+// modifier = Modifier.fillMaxWidth(),
+// horizontalArrangement = Arrangement.SpaceAround
+// ) {
+// TextButton(
+// onClick = {
+// onAction(PasscodeSetupUiAction.OnCancel)
+// }
+// ) {
+// Text(
+// text = "Cancel",
+// modifier = Modifier.padding(8.dp),
+// style = TextStyle(
+// fontSize = 16.sp,
+// fontWeight = FontWeight.Bold,
+// ),
+// )
+// }
+//
+// TextButton(
+// enabled = state.passcode.isNotEmpty(),
+// onClick = {
+// onAction(PasscodeSetupUiAction.OnBackspaceClick)
+// }
+// ) {
+// Text(
+// text = "Delete",
+// modifier = Modifier.padding(8.dp),
+// style = TextStyle(
+// fontSize = 16.sp,
+// fontWeight = FontWeight.Bold
+// ),
+// )
+// }
+//
+//
+// }
}
}
}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupViewModel.kt
index 01d3e7a8..1af8b2ae 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupViewModel.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/passcode/passcode_setup/PasscodeSetupViewModel.kt
@@ -30,6 +30,7 @@ class PasscodeSetupViewModel(
is PasscodeSetupUiAction.OnNumberClick -> onNumberClick(action.number)
PasscodeSetupUiAction.OnBackspaceClick -> onBackspaceClick()
PasscodeSetupUiAction.OnCancel -> onCancel()
+ PasscodeSetupUiAction.OnSubmit -> onSubmit()
}
}
@@ -56,11 +57,11 @@ class PasscodeSetupViewModel(
else state.copy(passcode = state.passcode + number)
}
- // Process passcode only when the required length is reached
- if (_uiState.value.passcode.length == config.passcodeLength) {
- _uiState.update { it.copy(isProcessing = true) }
- processPasscodeEntry()
- }
+// // Process passcode only when the required length is reached
+// if (_uiState.value.passcode.length == config.passcodeLength) {
+// _uiState.update { it.copy(isProcessing = true) }
+// processPasscodeEntry()
+// }
}
private fun onBackspaceClick() {
@@ -75,6 +76,16 @@ class PasscodeSetupViewModel(
}
}
+ private fun onSubmit() {
+ val state = _uiState.value
+
+ // Ensure passcode length is correct before submission
+ if (state.passcode.length == config.passcodeLength) {
+ _uiState.update { it.copy(isProcessing = true) }
+ processPasscodeEntry()
+ }
+ }
+
private fun processPasscodeEntry() = viewModelScope.launch {
val state = uiState.value // current state
if (state.isConfirming) {
@@ -138,6 +149,7 @@ sealed class PasscodeSetupUiAction {
data class OnNumberClick(val number: String) : PasscodeSetupUiAction()
data object OnBackspaceClick : PasscodeSetupUiAction()
data object OnCancel : PasscodeSetupUiAction()
+ data object OnSubmit: PasscodeSetupUiAction()
}
sealed class PasscodeSetupUiEvent {
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/spaces/ServerOptionItem.kt b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/ServerOptionItem.kt
new file mode 100644
index 00000000..302646dd
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/ServerOptionItem.kt
@@ -0,0 +1,162 @@
+package net.opendasharchive.openarchive.features.spaces
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
+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.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowForward
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+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.dp
+import androidx.compose.ui.unit.sp
+import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.presentation.theme.DefaultBoxPreview
+
+@Composable
+fun ServerOptionItem(
+ @DrawableRes iconRes: Int,
+ title: String,
+ subtitle: String,
+ onClick: () -> Unit
+) {
+ // You can customize this look to match your original design
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ .clickable(onClick = onClick),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.background
+ ),
+ border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.onBackground),
+ shape = RoundedCornerShape(8.dp)
+ ) {
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(100.dp)
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .align(Alignment.Top)
+ .padding(top = 4.dp)
+ ) {
+ Icon(
+ painter = painterResource(id = iconRes),
+ contentDescription = null,
+ tint = colorResource(R.color.colorTertiary),
+ modifier = Modifier
+ .size(24.dp)
+
+ )
+ }
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Column(
+ modifier = Modifier
+ .align(Alignment.Top)
+ .weight(1f),
+ verticalArrangement = Arrangement.Top
+ ) {
+ Text(
+ text = title,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 18.sp
+ )
+
+ Text(
+ text = subtitle,
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp
+ )
+ }
+
+ Icon(
+ modifier = Modifier
+ .size(24.dp)
+ .align(Alignment.CenterVertically),
+ imageVector = Icons.Default.ArrowForward,
+ contentDescription = null,
+ )
+ }
+
+
+ }
+}
+
+@Preview
+@Composable
+private fun ServerOptionItemPreview() {
+ DefaultBoxPreview {
+
+ Column {
+ ServerOptionItem(
+ iconRes = R.drawable.ic_private_server,
+ title = stringResource(R.string.private_server),
+ subtitle = stringResource(R.string.send_directly_to_a_private_server),
+ onClick = {}
+ )
+ }
+
+
+ }
+}
+
+@Preview
+@Composable
+private fun ServerOptionsItemPreview() {
+ DefaultBoxPreview {
+
+ Column {
+ ServerOptionItem(
+ iconRes = R.drawable.ic_private_server,
+ title = stringResource(R.string.private_server),
+ subtitle = stringResource(R.string.send_directly_to_a_private_server),
+ onClick = {}
+ )
+
+ ServerOptionItem(
+ iconRes = R.drawable.ic_internet_archive,
+ title = stringResource(R.string.internet_archive),
+ subtitle = stringResource(R.string.upload_to_the_internet_archive),
+ onClick = {}
+ )
+
+ ServerOptionItem(
+ iconRes = R.drawable.ic_dweb,
+ title = stringResource(R.string.dweb_title),
+ subtitle = stringResource(R.string.dweb_description),
+ onClick = {}
+ )
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListFragment.kt
new file mode 100644
index 00000000..37b7d576
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListFragment.kt
@@ -0,0 +1,85 @@
+package net.opendasharchive.openarchive.features.spaces
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.compose.content
+import androidx.navigation.fragment.findNavController
+import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme
+import net.opendasharchive.openarchive.databinding.FragmentSpaceListBinding
+import net.opendasharchive.openarchive.db.Space
+import net.opendasharchive.openarchive.features.core.BaseFragment
+import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveActivity
+import net.opendasharchive.openarchive.services.gdrive.GDriveActivity
+import net.opendasharchive.openarchive.services.webdav.WebDavActivity
+
+class SpaceListFragment : BaseFragment() {
+
+ private lateinit var binding: FragmentSpaceListBinding
+
+ companion object {
+ const val EXTRA_DATA_SPACE = "space_id"
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+
+ binding = FragmentSpaceListBinding.inflate(inflater)
+
+
+ binding.composeViewSpaceList.setContent {
+
+ SaveAppTheme {
+
+ SpaceListScreen(
+ onSpaceClicked = { space ->
+ startSpaceAuthActivity(space.id)
+ },
+ )
+ }
+
+ }
+
+ return binding.root
+ }
+
+ override fun getToolbarTitle() = "Media Servers"
+
+ private fun startSpaceAuthActivity(spaceId: Long?) {
+ val space = Space.get(spaceId ?: return) ?: return
+
+ when (space.tType) {
+ Space.Type.INTERNET_ARCHIVE -> {
+ val intent = Intent(requireContext(), InternetArchiveActivity::class.java)
+ intent.putExtra(EXTRA_DATA_SPACE, space.id)
+ startActivity(intent)
+ }
+
+ Space.Type.GDRIVE -> {
+ val intent = Intent(requireContext(), GDriveActivity::class.java)
+ intent.putExtra(EXTRA_DATA_SPACE, space.id)
+ startActivity(intent)
+ }
+
+ Space.Type.WEBDAV -> {
+ val action =
+ SpaceListFragmentDirections.actionFragmentSpaceListToFragmentWebDav(spaceId)
+ findNavController().navigate(action)
+ }
+
+ else -> {
+ val intent = Intent(requireContext(), WebDavActivity::class.java)
+ intent.putExtra(EXTRA_DATA_SPACE, space.id)
+ startActivity(intent)
+ }
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListScreen.kt
new file mode 100644
index 00000000..cdf651d3
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListScreen.kt
@@ -0,0 +1,127 @@
+package net.opendasharchive.openarchive.features.spaces
+
+import android.content.res.Configuration
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+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.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview
+import net.opendasharchive.openarchive.db.Space
+import net.opendasharchive.openarchive.features.main.ui.components.SpaceIcon
+import net.opendasharchive.openarchive.features.main.ui.components.dummySpaceList
+
+@Composable
+fun SpaceListScreen(
+ onSpaceClicked: (Space) -> Unit,
+
+ ) {
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+
+ SpaceListScreenContent(
+ spaceList = Space.getAll().asSequence().toList(),
+ onSpaceClicked = onSpaceClicked
+ )
+ }
+
+}
+
+@Composable
+fun SpaceListScreenContent(
+ onSpaceClicked: (Space) -> Unit,
+ spaceList: List = emptyList()
+) {
+
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+
+ spaceList.forEach { space ->
+
+ SpaceListItem(
+ space = space,
+ onClick = {
+ onSpaceClicked(space)
+ }
+ )
+ }
+ }
+}
+
+@Composable
+fun SpaceListItem(
+ space: Space,
+ onClick: () -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ onClick()
+ },
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ SpaceIcon(
+ type = space.tType,
+ modifier = Modifier.size(42.dp)
+ )
+
+ Column(
+ verticalArrangement = Arrangement.Top
+ ) {
+ Text(
+ text = space.friendlyName,
+ color = MaterialTheme.colorScheme.onBackground,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.SemiBold,
+ lineHeight = 1.sp
+ )
+
+ Text(
+ text = space.tType.friendlyName,
+ color = MaterialTheme.colorScheme.onBackground,
+ fontSize = 14.sp,
+ lineHeight = 1.sp
+ )
+ }
+ }
+}
+
+@Preview(uiMode = UI_MODE_NIGHT_YES)
+@Composable
+private fun SpaceListScreenPreview() {
+
+ DefaultScaffoldPreview {
+
+ SpaceListScreenContent(
+ spaceList = dummySpaceList,
+ onSpaceClicked = {
+
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceSetupScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceSetupScreen.kt
new file mode 100644
index 00000000..f35960a8
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceSetupScreen.kt
@@ -0,0 +1,106 @@
+package net.opendasharchive.openarchive.features.spaces
+
+import androidx.compose.foundation.layout.Column
+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.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+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.presentation.theme.DefaultScaffoldPreview
+
+@Composable
+fun SpaceSetupScreen(
+ onWebDavClick: () -> Unit,
+ isInternetArchiveAllowed: Boolean,
+ onInternetArchiveClick: () -> Unit,
+ isDwebEnabled: Boolean,
+ onDwebClicked: () -> Unit
+) {
+ // Use a scrollable Column to mimic ScrollView + LinearLayout
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(8.dp)
+ ) {
+ Spacer(modifier = Modifier.height(48.dp))
+ // Header texts
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.to_get_started_connect_to_a_server_to_store_your_media),
+ fontSize = 18.sp,
+ fontWeight = FontWeight.SemiBold,
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+
+ val description = if (isDwebEnabled) stringResource(R.string.to_get_started_more_hint_dweb) else stringResource(R.string.to_get_started_more_hint)
+ Text(
+ text = description,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Medium,
+ textAlign = TextAlign.Center
+ )
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ // WebDav option
+ ServerOptionItem(
+ iconRes = R.drawable.ic_private_server,
+ title = stringResource(R.string.private_server),
+ subtitle = stringResource(R.string.send_directly_to_a_private_server),
+ onClick = onWebDavClick
+ )
+
+
+ // Internet Archive option (conditionally visible)
+ if (isInternetArchiveAllowed) {
+ ServerOptionItem(
+ iconRes = R.drawable.ic_internet_archive,
+ title = stringResource(R.string.internet_archive),
+ subtitle = stringResource(R.string.upload_to_the_internet_archive),
+ onClick = onInternetArchiveClick
+ )
+ }
+
+ // Snowbird (Raven) option (conditionally visible)
+ if (isDwebEnabled) {
+ ServerOptionItem(
+ iconRes = R.drawable.ic_dweb,
+ title = stringResource(R.string.dweb_title),
+ subtitle = stringResource(R.string.dweb_description),
+ onClick = onDwebClicked
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun SpaceSetupScreenPreview() {
+ DefaultScaffoldPreview {
+ SpaceSetupScreen(
+ onWebDavClick = {},
+ isInternetArchiveAllowed = true,
+ onInternetArchiveClick = {},
+ isDwebEnabled = true,
+ onDwebClicked = {},
+ )
+ }
+}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpacesActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpacesActivity.kt
deleted file mode 100644
index db97e20b..00000000
--- a/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpacesActivity.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package net.opendasharchive.openarchive.features.spaces
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.recyclerview.widget.LinearLayoutManager
-import net.opendasharchive.openarchive.R
-import net.opendasharchive.openarchive.SpaceAdapter
-import net.opendasharchive.openarchive.SpaceAdapterListener
-import net.opendasharchive.openarchive.SpaceItemDecoration
-import net.opendasharchive.openarchive.databinding.ActivitySpacesBinding
-import net.opendasharchive.openarchive.db.Space
-import net.opendasharchive.openarchive.features.core.BaseActivity
-import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveActivity
-import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity
-import net.opendasharchive.openarchive.services.gdrive.GDriveActivity
-import net.opendasharchive.openarchive.services.webdav.WebDavActivity
-
-class SpacesActivity : BaseActivity(), SpaceAdapterListener {
-
- private lateinit var mBinding: ActivitySpacesBinding
- private lateinit var mAdapter: SpaceAdapter
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
-
- mBinding = ActivitySpacesBinding.inflate(layoutInflater)
- setContentView(mBinding.root)
-
- setupToolbar(title = "Servers", showBackButton = true)
-
- mAdapter = SpaceAdapter(context = this, listener = this)
-
- mBinding.rvProjects.layoutManager = LinearLayoutManager(this)
- val spacing = resources.getDimensionPixelSize(R.dimen.list_item_spacing)
- mBinding.rvProjects.addItemDecoration(SpaceItemDecoration(spacing))
- mBinding.rvProjects.adapter = mAdapter
-
-
- mBinding.fabAdd.setOnClickListener {
- startActivity(Intent(this, SpaceSetupActivity::class.java))
- }
- }
-
- override fun onResume() {
- super.onResume()
-
- val projects = Space.Companion.getAll().asSequence().toList()
-
- mAdapter.update(projects)
- }
-
- override fun spaceClicked(space: Space) {
- Space.Companion.current = space
- finish()
- }
-
- override fun editSpaceClicked(spaceId: Long?) {
- startSpaceAuthActivity(spaceId)
- }
-
- override fun getSelectedSpace(): Space? {
- return Space.Companion.current
- }
-
- private fun startSpaceAuthActivity(spaceId: Long?) {
- val space = Space.Companion.get(spaceId ?: return) ?: return
-
- val clazz = when (space.tType) {
- Space.Type.INTERNET_ARCHIVE -> InternetArchiveActivity::class.java
- Space.Type.GDRIVE -> GDriveActivity::class.java
- else -> WebDavActivity::class.java
- }
-
- val intent = Intent(this@SpacesActivity, clazz)
- intent.putExtra(EXTRA_DATA_SPACE, space.id)
-
- startActivity(intent)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveFragment.kt
index e0212300..4daff4b4 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
@@ -10,6 +10,7 @@ import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.text.HtmlCompat
import androidx.fragment.app.setFragmentResult
+import androidx.navigation.fragment.findNavController
import com.google.android.gms.auth.api.Auth
import com.google.android.gms.auth.api.signin.GoogleSignIn
import kotlinx.coroutines.CoroutineScope
@@ -56,7 +57,11 @@ class GDriveFragment : BaseFragment() {
mBinding.error.visibility = View.GONE
mBinding.btBack.setOnClickListener {
- setFragmentResult(RESP_CANCEL, bundleOf())
+ if(isJetpackNavigation) {
+ findNavController().popBackStack()
+ } else {
+ setFragmentResult(RESP_CANCEL, bundleOf())
+ }
}
mBinding.btAuthenticate.setOnClickListener {
@@ -79,7 +84,13 @@ class GDriveFragment : BaseFragment() {
)
} else {
// permission was already granted, we're already signed in, continue.
- setFragmentResult(RESP_AUTHENTICATED, bundleOf())
+ if (isJetpackNavigation) {
+ val message = getString(R.string.you_have_successfully_connected_to_gdrive)
+ val action = GDriveFragmentDirections.actionFragmentGdriveToFragmentSpaceSetupSuccess(message)
+ findNavController().navigate(action)
+ } else {
+ setFragmentResult(RESP_AUTHENTICATED, bundleOf())
+ }
}
}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdCreateGroupFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdCreateGroupFragment.kt
index 60bd9e35..ce0fa6bb 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdCreateGroupFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdCreateGroupFragment.kt
@@ -9,6 +9,7 @@ import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.launch
import net.opendasharchive.openarchive.databinding.FragmentSnowbirdCreateGroupBinding
import net.opendasharchive.openarchive.db.SnowbirdError
@@ -19,7 +20,7 @@ import net.opendasharchive.openarchive.util.FullScreenOverlayCreateGroupManager
import net.opendasharchive.openarchive.util.Utility
import timber.log.Timber
-class SnowbirdCreateGroupFragment private constructor() : BaseFragment() {
+class SnowbirdCreateGroupFragment: BaseFragment() {
private lateinit var viewBinding: FragmentSnowbirdCreateGroupBinding
@@ -133,14 +134,21 @@ class SnowbirdCreateGroupFragment private constructor() : BaseFragment() {
negativeButtonText = "No",
completion = { affirm ->
if (affirm) {
- setFragmentResult(
- RESULT_REQUEST_KEY,
- bundleOf(
- RESULT_NAVIGATION_KEY to RESULT_NAVIGATION_VAL_SHARE_SCREEN,
- RESULT_BUNDLE_GROUP_KEY to group.key
+ if (isJetpackNavigation) {
+ val action =
+ SnowbirdCreateGroupFragmentDirections.actionFragmentSnowbirdCreateGroupToFragmentSnowbirdShareGroup(
+ group.key
+ )
+ findNavController().navigate(action)
+ } else {
+ setFragmentResult(
+ RESULT_REQUEST_KEY,
+ bundleOf(
+ RESULT_NAVIGATION_KEY to RESULT_NAVIGATION_VAL_SHARE_SCREEN,
+ RESULT_BUNDLE_GROUP_KEY to group.key
+ )
)
- )
- //findNavController().navigate(SnowbirdCreateGroupFragmentDirections.navigateToShareScreen(group.key))
+ }
} else {
parentFragmentManager.popBackStack()
}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdFileListFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdFileListFragment.kt
index db96b431..3096c750 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdFileListFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdFileListFragment.kt
@@ -239,9 +239,8 @@ class SnowbirdFileListFragment : BaseFragment() {
}
companion object {
- const val RESULT_REQUEST_KEY = "raven_fragment_file_list_result"
- const val RESULT_VAL_RAVEN_GROUP_KEY = "raven_fragment_file_list_group_key"
- const val RESULT_VAL_RAVEN_REPO_KEY = "raven_fragment_file_list_repo_key"
+ const val RESULT_VAL_RAVEN_GROUP_KEY = "dweb_group_key"
+ const val RESULT_VAL_RAVEN_REPO_KEY = "dweb_repo_key"
@JvmStatic
fun newInstance(groupKey: String, repoKey: String) =
diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdFragment.kt
index 00addc61..213eba33 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdFragment.kt
@@ -10,6 +10,7 @@ import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.findNavController
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.launch
import net.opendasharchive.openarchive.databinding.FragmentSnowbirdBinding
@@ -20,8 +21,9 @@ import net.opendasharchive.openarchive.features.core.BaseFragment
import net.opendasharchive.openarchive.util.Utility
import timber.log.Timber
-class SnowbirdFragment private constructor(): BaseFragment() {
- private val CANNED_URI = "save+dweb::?dht=82fd345d484393a96b6e0c5d5e17a85a61c9184cc5a3311ab069d6efa0bf1410&enc=6fa27396fe298f92c91013ac54d8f316c2d45dc3bed0edec73078040aa10feed&pk=f4b404d294817cf11ea7f8ef7231626e03b74f6fafe3271b53918608afa82d12&sk=5482a8f490081be684fbadb8bde7f0a99bab8acdcf1ec094826f0f18e327e399"
+class SnowbirdFragment : BaseFragment() {
+ private val CANNED_URI =
+ "save+dweb::?dht=82fd345d484393a96b6e0c5d5e17a85a61c9184cc5a3311ab069d6efa0bf1410&enc=6fa27396fe298f92c91013ac54d8f316c2d45dc3bed0edec73078040aa10feed&pk=f4b404d294817cf11ea7f8ef7231626e03b74f6fafe3271b53918608afa82d12&sk=5482a8f490081be684fbadb8bde7f0a99bab8acdcf1ec094826f0f18e327e399"
private lateinit var viewBinding: FragmentSnowbirdBinding
private var canNavigate = false
@@ -36,7 +38,11 @@ class SnowbirdFragment private constructor(): BaseFragment() {
}
}
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
viewBinding = FragmentSnowbirdBinding.inflate(inflater)
return viewBinding.root
@@ -51,18 +57,29 @@ class SnowbirdFragment private constructor(): BaseFragment() {
viewBinding.myGroupsButton.setOnClickListener {
- setFragmentResult(
- RESULT_REQUEST_KEY,
- bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_RAVEN_MY_GROUPS)
- )
+ if (isJetpackNavigation) {
+ val action =
+ SnowbirdFragmentDirections.actionFragmentSnowbirdToFragmentSnowbirdGroupList()
+ findNavController().navigate(action)
+ } else {
+ setFragmentResult(
+ RESULT_REQUEST_KEY,
+ bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_RAVEN_MY_GROUPS)
+ )
+ }
}
viewBinding.createGroupButton.setOnClickListener {
-
- setFragmentResult(
- RESULT_REQUEST_KEY,
- bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_RAVEN_CREATE_GROUP)
- )
+ if (isJetpackNavigation) {
+ val action =
+ SnowbirdFragmentDirections.actionFragmentSnowbirdToFragmentSnowbirdCreateGroup()
+ findNavController().navigate(action)
+ } else {
+ setFragmentResult(
+ RESULT_REQUEST_KEY,
+ bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_RAVEN_CREATE_GROUP)
+ )
+ }
}
initializeViewModelObservers()
@@ -71,7 +88,13 @@ class SnowbirdFragment private constructor(): BaseFragment() {
private fun initializeViewModelObservers() {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
- launch { snowbirdGroupViewModel.groupState.collect { state -> handleGroupStateUpdate(state) } }
+ launch {
+ snowbirdGroupViewModel.groupState.collect { state ->
+ handleGroupStateUpdate(
+ state
+ )
+ }
+ }
}
}
}
@@ -106,21 +129,35 @@ class SnowbirdFragment private constructor(): BaseFragment() {
if (name == null) {
Utility.showMaterialWarning(
requireContext(),
- "Unable to determine group name from QR code.")
+ "Unable to determine group name from QR code."
+ )
return
}
if (SnowbirdGroup.exists(name)) {
Utility.showMaterialWarning(
requireContext(),
- "You have already joined this group.")
+ "You have already joined this group."
+ )
return
}
- setFragmentResult(
- RESULT_REQUEST_KEY,
- bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_RAVEN_JOIN_GROUPS, RESULT_VAL_RAVEN_JOIN_GROUPS_ARG to uriString)
- )
+ if (isJetpackNavigation) {
+ val action =
+ SnowbirdFragmentDirections.actionFragmentSnowbirdToFragmentSnowbirdJoinGroup(
+ uriString
+ )
+ findNavController().navigate(action)
+ } else {
+
+ setFragmentResult(
+ RESULT_REQUEST_KEY,
+ bundleOf(
+ RESULT_BUNDLE_KEY to RESULT_VAL_RAVEN_JOIN_GROUPS,
+ RESULT_VAL_RAVEN_JOIN_GROUPS_ARG to uriString
+ )
+ )
+ }
}
companion object {
diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdGroupListFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdGroupListFragment.kt
index 01cce039..1a0f1440 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdGroupListFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdGroupListFragment.kt
@@ -13,11 +13,12 @@ import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.launch
import net.opendasharchive.openarchive.R
import net.opendasharchive.openarchive.core.logger.AppLogger
-import net.opendasharchive.openarchive.databinding.FragmentSnowbirdListGroupsBinding
+import net.opendasharchive.openarchive.databinding.FragmentSnowbirdGroupListBinding
import net.opendasharchive.openarchive.db.SnowbirdError
import net.opendasharchive.openarchive.db.SnowbirdGroup
import net.opendasharchive.openarchive.features.core.BaseFragment
@@ -25,9 +26,9 @@ import net.opendasharchive.openarchive.util.SpacingItemDecoration
import net.opendasharchive.openarchive.util.Utility
import timber.log.Timber
-class SnowbirdGroupListFragment private constructor(): BaseFragment() {
+class SnowbirdGroupListFragment : BaseFragment() {
- private lateinit var viewBinding: FragmentSnowbirdListGroupsBinding
+ private lateinit var viewBinding: FragmentSnowbirdGroupListBinding
private lateinit var adapter: SnowbirdGroupsAdapter
override fun onCreateView(
@@ -35,7 +36,7 @@ class SnowbirdGroupListFragment private constructor(): BaseFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- viewBinding = FragmentSnowbirdListGroupsBinding.inflate(inflater)
+ viewBinding = FragmentSnowbirdGroupListBinding.inflate(inflater)
return viewBinding.root
}
@@ -71,11 +72,16 @@ class SnowbirdGroupListFragment private constructor(): BaseFragment() {
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_add -> {
- setFragmentResult(
- RESULT_REQUEST_KEY,
- bundleOf(RESULT_BUNDLE_NAVIGATION_KEY to RESULT_VAL_RAVEN_CREATE_GROUP_SCREEN)
- )
- //findNavController().navigate(SnowbirdGroupListFragmentDirections.navigateToSnowbirdCreateGroupScreen())
+ if (isJetpackNavigation) {
+ val action =
+ SnowbirdGroupListFragmentDirections.actionFragmentSnowbirdGroupListToFragmentSnowbirdCreateGroup()
+ findNavController().navigate(action)
+ } else {
+ setFragmentResult(
+ RESULT_REQUEST_KEY,
+ bundleOf(RESULT_BUNDLE_NAVIGATION_KEY to RESULT_VAL_RAVEN_CREATE_GROUP_SCREEN)
+ )
+ }
true
}
@@ -105,14 +111,17 @@ class SnowbirdGroupListFragment private constructor(): BaseFragment() {
}
private fun onClick(groupKey: String) {
- setFragmentResult(
- RESULT_REQUEST_KEY, bundleOf(
- RESULT_BUNDLE_NAVIGATION_KEY to RESULT_VAL_RAVEN_REPO_LIST_SCREEN,
- RESULT_BUNDLE_GROUP_KEY to groupKey
+ if (isJetpackNavigation) {
+ val action = SnowbirdGroupListFragmentDirections.actionFragmentSnowbirdGroupListToFragmentSnowbirdListRepos(groupKey)
+ findNavController().navigate(action)
+ } else {
+ setFragmentResult(
+ RESULT_REQUEST_KEY, bundleOf(
+ RESULT_BUNDLE_NAVIGATION_KEY to RESULT_VAL_RAVEN_REPO_LIST_SCREEN,
+ RESULT_BUNDLE_GROUP_KEY to groupKey
+ )
)
- )
- //findNavController()
- // .navigate(SnowbirdGroupListFragmentDirections.navigateToSnowbirdListReposScreen(groupKey))
+ }
}
private fun onLongPress(groupKey: String) {
@@ -125,12 +134,18 @@ class SnowbirdGroupListFragment private constructor(): BaseFragment() {
negativeButtonText = "No"
) { affirm ->
if (affirm) {
- setFragmentResult(RESULT_REQUEST_KEY,
- bundleOf(
- RESULT_BUNDLE_NAVIGATION_KEY to RESULT_VAL_RAVEN_SHARE_SCREEN,
- RESULT_BUNDLE_GROUP_KEY to groupKey
+ if (isJetpackNavigation) {
+ val action = SnowbirdGroupListFragmentDirections.actionFragmentSnowbirdGroupListToFragmentSnowbirdShareGroup(groupKey)
+ findNavController().navigate(action)
+ } else {
+ setFragmentResult(
+ RESULT_REQUEST_KEY,
+ bundleOf(
+ RESULT_BUNDLE_NAVIGATION_KEY to RESULT_VAL_RAVEN_SHARE_SCREEN,
+ RESULT_BUNDLE_GROUP_KEY to groupKey
+ )
)
- )
+ }
//findNavController().navigate(SnowbirdGroupListFragmentDirections.navigateToSnowbirdShareScreen(groupKey))
}
}
@@ -198,7 +213,7 @@ class SnowbirdGroupListFragment private constructor(): BaseFragment() {
const val RESULT_VAL_RAVEN_REPO_LIST_SCREEN = "raven_repo_list_screen"
const val RESULT_VAL_RAVEN_SHARE_SCREEN = "raven_share_group_screen"
- const val RESULT_BUNDLE_GROUP_KEY = "raven_group_list_fragment_bundle_group_id"
+ const val RESULT_BUNDLE_GROUP_KEY = "dweb_group_key"
@JvmStatic
fun newInstance() = SnowbirdGroupListFragment()
diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdJoinGroupFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdJoinGroupFragment.kt
index 03abe0a8..95414362 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdJoinGroupFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdJoinGroupFragment.kt
@@ -19,7 +19,7 @@ import net.opendasharchive.openarchive.util.FullScreenOverlayCreateGroupManager
import net.opendasharchive.openarchive.util.Utility
import timber.log.Timber
-class SnowbirdJoinGroupFragment private constructor(): BaseFragment() {
+class SnowbirdJoinGroupFragment: BaseFragment() {
private lateinit var viewBinding: FragmentSnowbirdJoinGroupBinding
private lateinit var uriString: String
diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdRepoListFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdRepoListFragment.kt
index b0203fc9..e3e129b1 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdRepoListFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdRepoListFragment.kt
@@ -13,6 +13,7 @@ import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.launch
import net.opendasharchive.openarchive.R
@@ -25,7 +26,7 @@ import net.opendasharchive.openarchive.util.SpacingItemDecoration
import net.opendasharchive.openarchive.util.Utility
import timber.log.Timber
-class SnowbirdRepoListFragment private constructor() : BaseFragment() {
+class SnowbirdRepoListFragment: BaseFragment() {
private lateinit var viewBinding: FragmentSnowbirdListReposBinding
private lateinit var adapter: SnowbirdRepoListAdapter
@@ -114,13 +115,22 @@ class SnowbirdRepoListFragment private constructor() : BaseFragment() {
adapter = SnowbirdRepoListAdapter { repoKey ->
AppLogger.d("Click!!")
//findNavController().navigate(SnowbirdRepoListFragmentDirections.navigateToSnowbirdListFilesScreen(groupKey, repoKey))
- setFragmentResult(
- RESULT_REQUEST_KEY,
- bundleOf(
- RESULT_VAL_RAVEN_GROUP_KEY to groupKey,
- RESULT_VAL_RAVEN_REPO_KEY to repoKey
+ if (isJetpackNavigation) {
+ val action =
+ SnowbirdRepoListFragmentDirections.actionFragmentSnowbirdListReposToFragmentSnowbirdListMedia(
+ dwebGroupKey = groupKey,
+ dwebRepoKey = repoKey
+ )
+ findNavController().navigate(action)
+ } else {
+ setFragmentResult(
+ RESULT_REQUEST_KEY,
+ bundleOf(
+ RESULT_VAL_RAVEN_GROUP_KEY to groupKey,
+ RESULT_VAL_RAVEN_REPO_KEY to repoKey
+ )
)
- )
+ }
}
val spacingInPixels = resources.getDimensionPixelSize(R.dimen.list_item_spacing)
@@ -190,8 +200,8 @@ class SnowbirdRepoListFragment private constructor() : BaseFragment() {
companion object {
const val RESULT_REQUEST_KEY = "raven_fragment_repo_list_result"
- const val RESULT_VAL_RAVEN_GROUP_KEY = "raven_fragment_repo_list_group_key"
- const val RESULT_VAL_RAVEN_REPO_KEY = "raven_fragment_repo_list_repo_key"
+ const val RESULT_VAL_RAVEN_GROUP_KEY = "dweb_group_key"
+ const val RESULT_VAL_RAVEN_REPO_KEY = "dweb_repo_key"
@JvmStatic
fun newInstance(groupKey: String) =
diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdShareFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdShareFragment.kt
index 6800556e..e0ece706 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdShareFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/SnowbirdShareFragment.kt
@@ -10,7 +10,7 @@ import net.opendasharchive.openarchive.extensions.asQRCode
import net.opendasharchive.openarchive.extensions.urlEncode
import net.opendasharchive.openarchive.features.core.BaseFragment
-class SnowbirdShareFragment private constructor(): BaseFragment() {
+class SnowbirdShareFragment: BaseFragment() {
private lateinit var viewBinding: FragmentSnowbirdShareGroupBinding
private lateinit var groupKey: String
@@ -48,7 +48,7 @@ class SnowbirdShareFragment private constructor(): BaseFragment() {
companion object {
- const val RESULT_VAL_RAVEN_GROUP_KEY = "RESULT_VAL_RAVEN_GROUP_KEY"
+ const val RESULT_VAL_RAVEN_GROUP_KEY = "dweb_group_key"
@JvmStatic
fun newInstance(groupKey: String): SnowbirdShareFragment {
diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt
index 7e3bc966..f75a9a9f 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt
@@ -1,27 +1,42 @@
package net.opendasharchive.openarchive.services.webdav
-import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
-import android.view.inputmethod.InputMethodManager
+import androidx.activity.addCallback
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Warning
import androidx.core.os.bundleOf
+import androidx.core.view.MenuProvider
import androidx.fragment.app.setFragmentResult
+import androidx.lifecycle.Lifecycle
+import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import net.opendasharchive.openarchive.BuildConfig
import net.opendasharchive.openarchive.R
+import net.opendasharchive.openarchive.core.logger.AppLogger
import net.opendasharchive.openarchive.databinding.FragmentWebDavBinding
import net.opendasharchive.openarchive.db.Space
import net.opendasharchive.openarchive.features.core.BaseFragment
+import net.opendasharchive.openarchive.features.core.UiImage
+import net.opendasharchive.openarchive.features.core.UiText
+import net.opendasharchive.openarchive.features.core.asUiText
+import net.opendasharchive.openarchive.features.core.dialog.ButtonData
+import net.opendasharchive.openarchive.features.core.dialog.DialogConfig
+import net.opendasharchive.openarchive.features.core.dialog.DialogType
+import net.opendasharchive.openarchive.features.core.dialog.showDialog
+import net.opendasharchive.openarchive.features.settings.CreativeCommonsLicenseManager
import net.opendasharchive.openarchive.services.SaveClient
import net.opendasharchive.openarchive.services.internetarchive.Util
-import net.opendasharchive.openarchive.util.AlertHelper
-import net.opendasharchive.openarchive.util.Utility
import net.opendasharchive.openarchive.util.extensions.makeSnackBar
import okhttp3.Call
import okhttp3.Callback
@@ -37,6 +52,9 @@ class WebDavFragment : BaseFragment() {
private lateinit var mSnackbar: Snackbar
private lateinit var binding: FragmentWebDavBinding
+ private var originalName: String? = null
+ private var isNameChanged = false
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mSpaceId = arguments?.getLong(ARG_SPACE_ID) ?: ARG_VAL_NEW_SPACE
@@ -51,7 +69,7 @@ class WebDavFragment : BaseFragment() {
mSpaceId = arguments?.getLong(ARG_SPACE_ID) ?: ARG_VAL_NEW_SPACE
if (mSpaceId != ARG_VAL_NEW_SPACE) {
- // setup views for editing and existing space
+ // setup views for editing an existing space
mSpace = Space.get(mSpaceId!!) ?: Space(Space.Type.WEBDAV)
@@ -71,6 +89,7 @@ class WebDavFragment : BaseFragment() {
binding.password.setText(mSpace.password)
binding.name.setText(mSpace.name)
+ binding.layoutName.visibility = View.VISIBLE
// mBinding.swChunking.isChecked = mSpace.useChunking
// mBinding.swChunking.setOnCheckedChangeListener { _, useChunking ->
@@ -80,41 +99,66 @@ class WebDavFragment : BaseFragment() {
binding.btRemove.setOnClickListener {
- removeProject()
+ removeSpace()
}
// swap webDavFragment with Creative Commons License Fragment
- binding.btLicense.setOnClickListener {
- setFragmentResult(RESP_LICENSE, bundleOf())
- }
+// binding.btLicense.setOnClickListener {
+// setFragmentResult(RESP_LICENSE, bundleOf())
+// }
- binding.name.setOnEditorActionListener { _, actionId, _ ->
- if (actionId == EditorInfo.IME_ACTION_DONE) {
-
- val enteredName = binding.name.text?.toString()?.trim()
- if (!enteredName.isNullOrEmpty()) {
- // Update the Space entity and save it using SugarORM
- mSpace.name = enteredName
- mSpace.save() // Save the entity using SugarORM
-
- // Hide the keyboard
- val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
- imm.hideSoftInputFromWindow(binding.name.windowToken, 0)
- binding.name.clearFocus() // Clear focus from the input field
-
- // Optional: Provide feedback to the user
- Snackbar.make(binding.root, "Name saved successfully!", Snackbar.LENGTH_SHORT).show()
- } else {
- // Notify the user that the name cannot be empty (optional)
- Snackbar.make(binding.root, "Name cannot be empty", Snackbar.LENGTH_SHORT).show()
- }
+// binding.name.setOnEditorActionListener { _, actionId, _ ->
+// if (actionId == EditorInfo.IME_ACTION_DONE) {
+//
+// val enteredName = binding.name.text?.toString()?.trim()
+// if (!enteredName.isNullOrEmpty()) {
+// // Update the Space entity and save it using SugarORM
+// mSpace.name = enteredName
+// mSpace.save() // Save the entity using SugarORM
+//
+// // Hide the keyboard
+// val imm =
+// requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+// imm.hideSoftInputFromWindow(binding.name.windowToken, 0)
+// binding.name.clearFocus() // Clear focus from the input field
+//
+// // Optional: Provide feedback to the user
+// Snackbar.make(
+// binding.root,
+// "Name saved successfully!",
+// Snackbar.LENGTH_SHORT
+// ).show()
+// } else {
+// // Notify the user that the name cannot be empty (optional)
+// Snackbar.make(binding.root, "Name cannot be empty", Snackbar.LENGTH_SHORT)
+// .show()
+// }
+//
+// true // Consume the event
+// } else {
+// false // Pass the event to the next listener
+// }
+// }
- true // Consume the event
- } else {
- false // Pass the event to the next listener
+ originalName = mSpace.name
+
+ // Listen for name changes
+ binding.name.addTextChangedListener(object : android.text.TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ val enteredName = s?.toString()?.trim()
+ isNameChanged = enteredName != originalName
+ requireActivity().invalidateOptionsMenu() // Refresh menu to show confirm button
}
- }
+ override fun afterTextChanged(s: android.text.Editable?) {}
+ })
+
+ CreativeCommonsLicenseManager.initialize(binding.cc, mSpace.license) {
+ mSpace.license = it
+ mSpace.save()
+ }
} else {
// setup views for creating a new space
@@ -122,14 +166,22 @@ class WebDavFragment : BaseFragment() {
binding.btRemove.visibility = View.GONE
binding.buttonBar.visibility = View.VISIBLE
binding.buttonBarEdit.visibility = View.GONE
+ binding.layoutName.visibility = View.GONE
+ binding.layoutLicense.visibility = View.GONE
+
+ binding.btAuthenticate.isEnabled = false
+ setupTextWatchers()
- binding.name.visibility = View.GONE
}
binding.btAuthenticate.setOnClickListener { attemptLogin() }
binding.btCancel.setOnClickListener {
- setFragmentResult(RESP_CANCEL, bundleOf())
+ if (isJetpackNavigation) {
+ findNavController().popBackStack()
+ } else {
+ setFragmentResult(RESP_CANCEL, bundleOf())
+ }
}
binding.server.setOnFocusChangeListener { _, hasFocus ->
@@ -140,7 +192,7 @@ class WebDavFragment : BaseFragment() {
binding.password.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
- attemptLogin()
+ //attemptLogin()
}
false
@@ -152,6 +204,101 @@ class WebDavFragment : BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mSnackbar = binding.root.makeSnackBar(getString(R.string.login_activity_logging_message))
+
+ if (mSpaceId != ARG_VAL_NEW_SPACE) {
+ val menuProvider = object : MenuProvider {
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.menu_confirm, menu)
+ }
+
+ override fun onPrepareMenu(menu: Menu) {
+ super.onPrepareMenu(menu)
+ val btnConfirm = menu.findItem(R.id.action_confirm)
+ btnConfirm?.isVisible = isNameChanged
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ return when (menuItem.itemId) {
+ R.id.action_confirm -> {
+ //todo: save changes here and show success dialog
+ saveChanges()
+ true
+ }
+ android.R.id.home -> {
+ if(isNameChanged) {
+ AppLogger.e("unsaved changes")
+ showUnsavedChangesDialog()
+ false
+ } else {
+ findNavController().popBackStack()
+ }
+ }
+ else -> false
+ }
+ }
+ }
+
+ requireActivity().addMenuProvider(
+ menuProvider,
+ viewLifecycleOwner,
+ Lifecycle.State.RESUMED
+ )
+
+
+ requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
+ if (isNameChanged) {
+ showUnsavedChangesDialog()
+ } else {
+ findNavController().popBackStack()
+ }
+ }
+ }
+ }
+
+ private fun saveChanges() {
+ val enteredName = binding.name.text?.toString()?.trim()
+ if (!enteredName.isNullOrEmpty()) {
+ mSpace.name = enteredName
+ mSpace.save()
+ originalName = enteredName
+ isNameChanged = false
+ requireActivity().invalidateOptionsMenu() //Refresh menu to hide confirm btn again
+ showSuccessDialog()
+ } else {
+ Snackbar.make(binding.root, "Name cannot be empty", Snackbar.LENGTH_LONG).show()
+ }
+ }
+
+ private fun showSuccessDialog() {
+ dialogManager.showDialog(dialogManager.requireResourceProvider()) {
+ type = DialogType.Success
+ title = R.string.label_success_title.asUiText()
+ message = R.string.msg_edit_server_success.asUiText()
+ icon = UiImage.DrawableResource(R.drawable.ic_done)
+ positiveButton {
+ text = UiText.StringResource(R.string.lbl_got_it)
+ action = {
+ findNavController().popBackStack()
+ }
+ }
+ }
+ }
+
+ private fun showUnsavedChangesDialog() {
+ dialogManager.showDialog(DialogConfig(
+ type = DialogType.Warning,
+ title = UiText.DynamicString("Unsaved changes!"),
+ message = UiText.DynamicString("Do you want to save"),
+ icon = UiImage.DynamicVector(Icons.Default.Warning),
+ positiveButton = ButtonData(
+ text = UiText.DynamicString("Save"),
+ action = { saveChanges() }
+ ),
+ neutralButton = ButtonData(
+ text = UiText.DynamicString("Discard"),
+ action = { findNavController().popBackStack() }
+ )
+ ))
}
private fun fixSpaceUrl(url: CharSequence?): Uri? {
@@ -193,8 +340,6 @@ class WebDavFragment : BaseFragment() {
mSpace.username = binding.username.text?.toString() ?: ""
mSpace.password = binding.password.text?.toString() ?: ""
-// mSpace.useChunking = mBinding.swChunking.isChecked
-
if (mSpace.host.isEmpty()) {
binding.server.error = getString(R.string.error_field_required)
errorView = binding.server
@@ -245,14 +390,22 @@ class WebDavFragment : BaseFragment() {
}
}
- private fun navigate(spaceId: Long) {
- Utility.showMaterialMessage(
- context = requireContext(),
- title = "Success",
- message = "You have successfully authenticated! Now let's continue setting up your media server."
- ) {
+ private fun navigate(spaceId: Long) = CoroutineScope(Dispatchers.Main).launch {
+// Utility.showMaterialMessage(
+// context = requireContext(),
+// title = "Success",
+// message = "You have successfully authenticated! Now let's continue setting up your media server."
+// ) {}
+ if (isJetpackNavigation) {
+ val action =
+ WebDavFragmentDirections.actionFragmentWebDavToFragmentWebDavSetupLicense(
+ spaceId = spaceId
+ )
+ findNavController().navigate(action)
+ } else {
setFragmentResult(RESP_SAVED, bundleOf(ARG_SPACE_ID to spaceId))
}
+
}
private suspend fun testConnection() {
@@ -304,6 +457,9 @@ class WebDavFragment : BaseFragment() {
override fun onStop() {
super.onStop()
+ if (isNameChanged) {
+ binding.name.requestFocus()
+ }
// make sure the snack-bar is gone when this fragment isn't on display anymore
mSnackbar.dismiss()
@@ -311,18 +467,52 @@ class WebDavFragment : BaseFragment() {
Util.hideSoftKeyboard(requireActivity())
}
- private fun removeProject() {
- AlertHelper.show(
- requireContext(),
- R.string.are_you_sure_you_want_to_remove_this_server_from_the_app,
- R.string.remove_from_app,
- buttons = listOf(
- AlertHelper.positiveButton(R.string.remove) { _, _ ->
+ private fun removeSpace() {
+ val config = DialogConfig(
+ type = DialogType.Warning,
+ title = R.string.remove_from_app.asUiText(),
+ message = R.string.are_you_sure_you_want_to_remove_this_server_from_the_app.asUiText(),
+ icon = UiImage.DrawableResource(R.drawable.ic_trash),
+ destructiveButton = ButtonData(
+ text = UiText.StringResource(R.string.lbl_ok),
+ action = {
mSpace.delete()
- setFragmentResult(RESP_DELETED, bundleOf())
- }, AlertHelper.negativeButton()
+ findNavController().popBackStack()
+ }
+ ),
+ neutralButton = ButtonData(
+ text = UiText.StringResource(R.string.lbl_Cancel),
+ action = {}
)
)
+ dialogManager.showDialog(config)
+ }
+
+ private fun setupTextWatchers() {
+ // Create a common TextWatcher for all three fields
+ val textWatcher = object : android.text.TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ updateAuthenticateButtonState()
+ }
+
+ override fun afterTextChanged(s: android.text.Editable?) {}
+ }
+
+ binding.server.addTextChangedListener(textWatcher)
+ binding.username.addTextChangedListener(textWatcher)
+ binding.password.addTextChangedListener(textWatcher)
+ }
+
+ private fun updateAuthenticateButtonState() {
+ val url = binding.server.text?.toString()?.trim().orEmpty()
+ val username = binding.username.text?.toString()?.trim().orEmpty()
+ val password = binding.password.text?.toString()?.trim().orEmpty()
+
+ // Enable the button only if none of the fields are empty
+ binding.btAuthenticate.isEnabled =
+ url.isNotEmpty() && username.isNotEmpty() && password.isNotEmpty()
}
companion object {
@@ -333,7 +523,7 @@ class WebDavFragment : BaseFragment() {
const val RESP_LICENSE = "web_dav_fragment_resp_license"
// factory method parameters (bundle args)
- const val ARG_SPACE_ID = "space"
+ const val ARG_SPACE_ID = "space_id"
const val ARG_VAL_NEW_SPACE = -1L
// other internal constants
@@ -351,8 +541,9 @@ class WebDavFragment : BaseFragment() {
}
override fun getToolbarTitle(): String = if (mSpaceId == ARG_VAL_NEW_SPACE) {
- "Add Private Server"
+ "Private Server"
} else {
- "Edit Private Server"
+ val space = Space.get(mSpaceId!!)
+ space?.name ?: "Private Server"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavSetupLicenseFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavSetupLicenseFragment.kt
index f48e1faf..9e9d9259 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavSetupLicenseFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavSetupLicenseFragment.kt
@@ -6,13 +6,15 @@ import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
+import androidx.navigation.fragment.findNavController
import net.opendasharchive.openarchive.R
import net.opendasharchive.openarchive.databinding.FragmentWebdavSetupLicenseBinding
import net.opendasharchive.openarchive.db.Space
import net.opendasharchive.openarchive.features.core.BaseFragment
-import net.opendasharchive.openarchive.features.settings.CcSelector
+import net.opendasharchive.openarchive.features.settings.CreativeCommonsLicenseManager
import kotlin.properties.Delegates
class WebDavSetupLicenseFragment: BaseFragment() {
@@ -39,18 +41,29 @@ class WebDavSetupLicenseFragment: BaseFragment() {
// Editing means hide subtitle, bottom bar buttons
binding.buttonBar.visibility = View.GONE
binding.descriptionText.visibility = View.GONE
+ } else {
+ binding.btCancel.visibility = View.INVISIBLE
}
binding.btNext.setOnClickListener {
- setFragmentResult(RESP_SAVED, bundleOf())
+ if (isJetpackNavigation) {
+ val action = WebDavSetupLicenseFragmentDirections.actionFragmentWebDavSetupLicenseToFragmentSpaceSetupSuccess(message = getString(R.string.you_have_successfully_connected_to_a_private_server))
+ findNavController().navigate(action)
+ } else {
+ setFragmentResult(RESP_SAVED, bundleOf())
+ }
}
binding.btCancel.setOnClickListener {
- setFragmentResult(RESP_CANCEL, bundleOf())
+ if (isJetpackNavigation) {
+ findNavController().popBackStack()
+ } else {
+ setFragmentResult(RESP_CANCEL, bundleOf())
+ }
}
- binding.cc.tvCc.setText(R.string.set_creative_commons_license_for_all_folders_on_this_server)
+ binding.cc.tvCcLabel.setText(R.string.set_creative_commons_license_for_all_folders_on_this_server)
return binding.root
}
@@ -82,12 +95,18 @@ class WebDavSetupLicenseFragment: BaseFragment() {
}
})
- CcSelector.init(binding.cc, Space.current?.license) {
- val space = Space.current ?: return@init
+ CreativeCommonsLicenseManager.initialize(binding.cc, Space.current?.license) {
+ val space = Space.current ?: return@initialize
space.license = it
space.save()
}
+
+ requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true){
+ override fun handleOnBackPressed() {
+ // do nothing
+ }
+ })
}
companion object {
@@ -97,7 +116,7 @@ class WebDavSetupLicenseFragment: BaseFragment() {
const val RESP_CANCEL = "webdav_setup_license_fragment_resp_cancel"
const val ARG_SPACE_ID = "space_id"
- const val ARG_IS_EDITING = "isEditing"
+ const val ARG_IS_EDITING = "is_editing"
@JvmStatic
fun newInstance(spaceId: Long, isEditing: Boolean) = WebDavSetupLicenseFragment().apply {
@@ -109,7 +128,7 @@ class WebDavSetupLicenseFragment: BaseFragment() {
}
}
- override fun getToolbarTitle() = "Select a License"
+ override fun getToolbarTitle() = getString(R.string.private_server)
override fun getToolbarSubtitle(): String? = null
override fun shouldShowBackButton() = false
}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/SKBottomSheetDialogFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/SKBottomSheetDialogFragment.kt
new file mode 100644
index 00000000..0aa1b83f
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/upload/SKBottomSheetDialogFragment.kt
@@ -0,0 +1,65 @@
+package net.opendasharchive.openarchive.upload
+
+import android.app.Dialog
+import android.content.res.Resources
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+
+open class SKBottomSheetDialogFragment : BottomSheetDialogFragment() {
+ override fun onStart() {
+ super.onStart()
+ val sheetContainer = requireView().parent as? ViewGroup ?: return
+ sheetContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
+
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val dialog = super.onCreateDialog(savedInstanceState)
+ dialog.setOnShowListener { dialogInterface ->
+ (dialogInterface as? BottomSheetDialog)?.let { bottomSheetDialog ->
+ (bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet)
+ as? FrameLayout)?.let { frameLayout ->
+
+ val behavior = BottomSheetBehavior.from(frameLayout)
+
+ // Set behavior attributes to allow collapsing and dismissing
+ behavior.peekHeight = Resources.getSystem().displayMetrics.heightPixels
+// behavior.peekHeight = 0 // Start from full-screen
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED // Initially expanded
+ behavior.isDraggable = false // Allow dragging
+ behavior.skipCollapsed = false // Enable collapse
+ behavior.isHideable = false // Allow dismissing
+
+ // Dismiss the dialog when hidden
+ behavior.addBottomSheetCallback(object :
+ BottomSheetBehavior.BottomSheetCallback() {
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_HIDDEN) {
+ dismiss()
+ }
+ }
+
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {
+ // Handle sliding behavior (optional)
+ }
+ })
+
+ // Handle edge-to-edge behavior
+ ViewCompat.setOnApplyWindowInsetsListener(frameLayout) { view, insets ->
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ view.setPadding(0, systemBars.top, 0, systemBars.bottom)
+ insets
+ }
+ }
+ }
+ }
+ return dialog
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerActivity.kt
index 493a1ce1..b72bb9f2 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerActivity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerActivity.kt
@@ -36,22 +36,11 @@ class UploadManagerActivity : BaseActivity() {
else {
handler.post { mFrag?.updateItem(mediaId) }
}
-
-// if (media?.sStatus == Media.Status.Error) {
-// CleanInsightsManager.getConsent(this@UploadManagerActivity) {
-// // TODO: Record metadata. See iOS implementation.
-// CleanInsightsManager.measureEvent("upload", "upload_failed")
-// }
-// }
- }
-
- handler.post {
- updateTitle()
}
}
}
- private var mEditMode = false
+ private var mEditMode = true // Setting Edit mode as the default mode
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -60,11 +49,15 @@ class UploadManagerActivity : BaseActivity() {
setContentView(mBinding.root)
setupToolbar(
- title = getString(R.string.uploads),
+ title = getString(R.string.upload_manager_screen_title),
+ subtitle = getString(R.string.upload_manager_screen_subtitle),
showBackButton = true
)
- mFrag = supportFragmentManager.findFragmentById(R.id.fragUploadManager) as? UploadManagerFragment
+ //mFrag = supportFragmentManager.findFragmentById(R.id.fragUploadManager) as? UploadManagerFragment
+
+ val bottomSheet = UploadManagerFragment()
+ bottomSheet.show(supportFragmentManager, UploadManagerFragment.TAG)
}
override fun onResume() {
@@ -73,7 +66,7 @@ class UploadManagerActivity : BaseActivity() {
BroadcastManager.register(this, mMessageReceiver)
- updateTitle()
+ onStartEdit()
}
override fun onPause() {
@@ -82,6 +75,14 @@ class UploadManagerActivity : BaseActivity() {
BroadcastManager.unregister(this, mMessageReceiver)
}
+ private fun onStartEdit() {
+ UploadService.stopUploadService(this)
+ }
+
+ private fun onCompleteEdit() {
+ UploadService.startUploadService(this)
+ }
+
private fun toggleEditMode() {
mEditMode = !mEditMode
mFrag?.setEditMode(mEditMode)
@@ -103,7 +104,7 @@ class UploadManagerActivity : BaseActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_upload, menu)
- mMenuEdit = menu.findItem(R.id.menu_edit)
+ mMenuEdit = menu.findItem(R.id.menu_done)
return super.onCreateOptionsMenu(menu)
}
@@ -114,8 +115,8 @@ class UploadManagerActivity : BaseActivity() {
finish()
return true
}
- R.id.menu_edit -> {
- toggleEditMode()
+ R.id.menu_done -> {
+ onCompleteEdit()
return true
}
}
@@ -135,8 +136,7 @@ class UploadManagerActivity : BaseActivity() {
if (mEditMode) {
supportActionBar?.title = getString(R.string.edit_media)
supportActionBar?.subtitle = getString(R.string.uploading_is_paused)
- }
- else {
+ } else {
val count = mFrag?.getUploadingCounter() ?: 0
supportActionBar?.title = if (count < 1) {
diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerFragment.kt
index cef0814b..c95f8d24 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerFragment.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerFragment.kt
@@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
-import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
@@ -13,18 +12,20 @@ import androidx.recyclerview.widget.RecyclerView
import net.opendasharchive.openarchive.R
import net.opendasharchive.openarchive.databinding.FragmentUploadManagerBinding
import net.opendasharchive.openarchive.db.Media
-import net.opendasharchive.openarchive.db.MediaAdapter
-import net.opendasharchive.openarchive.db.MediaViewHolder
+import net.opendasharchive.openarchive.db.UploadMediaAdapter
+import net.opendasharchive.openarchive.features.main.MainActivity
-open class UploadManagerFragment : Fragment() {
+open class UploadManagerFragment : SKBottomSheetDialogFragment() {
companion object {
- private val STATUSES = listOf(Media.Status.Uploading, Media.Status.Queued, Media.Status.Error)
+ const val TAG = "ModalBottomSheet-UploadManagerFragment"
+ private val STATUSES =
+ listOf(Media.Status.Uploading, Media.Status.Queued, Media.Status.Error)
}
- open var mediaAdapter: MediaAdapter? = null
+ open var uploadMediaAdapter: UploadMediaAdapter? = null
- private lateinit var mBinding: FragmentUploadManagerBinding
+ private lateinit var binding: FragmentUploadManagerBinding
private lateinit var mItemTouchHelper: ItemTouchHelper
@@ -33,32 +34,29 @@ open class UploadManagerFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
- mBinding = FragmentUploadManagerBinding.inflate(inflater, container, false)
+ binding = FragmentUploadManagerBinding.inflate(inflater, container, false)
- mBinding.uploadList.layoutManager = LinearLayoutManager(activity)
+ binding.uploadList.layoutManager = LinearLayoutManager(activity)
- val decorator = DividerItemDecoration(mBinding.uploadList.context, DividerItemDecoration.VERTICAL)
- val divider = ContextCompat.getDrawable(mBinding.uploadList.context, R.drawable.divider)
+ val decorator = DividerItemDecoration(binding.uploadList.context, DividerItemDecoration.VERTICAL)
+ val divider = ContextCompat.getDrawable(binding.uploadList.context, R.drawable.divider)
if (divider != null) decorator.setDrawable(divider)
- mBinding.uploadList.addItemDecoration(decorator)
- mBinding.uploadList.setHasFixedSize(true)
+ binding.uploadList.addItemDecoration(decorator)
+ binding.uploadList.setHasFixedSize(true)
- mediaAdapter =
- MediaAdapter(
- activity,
- { MediaViewHolder.SmallRow(it) },
- Media.getByStatus(STATUSES, Media.ORDER_PRIORITY),
- mBinding.uploadList,
- listOf(Media.Status.Error)
- )
+ uploadMediaAdapter = UploadMediaAdapter(
+ activity = activity,
+ mediaItems = Media.getByStatus(STATUSES, Media.ORDER_PRIORITY),
+ recyclerView = binding.uploadList,
+ )
- mediaAdapter?.doImageFade = false
- mBinding.uploadList.adapter = mediaAdapter
+ uploadMediaAdapter?.doImageFade = false
+ binding.uploadList.adapter = uploadMediaAdapter
mItemTouchHelper = ItemTouchHelper(object : SwipeToDeleteCallback(context) {
override fun isEditingAllowed(): Boolean {
- return mediaAdapter?.isEditMode ?: false
+ return uploadMediaAdapter?.isEditMode == true
}
override fun onMove(
@@ -66,7 +64,7 @@ open class UploadManagerFragment : Fragment() {
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
- mediaAdapter?.onItemMove(
+ uploadMediaAdapter?.onItemMove(
viewHolder.bindingAdapterPosition,
target.bindingAdapterPosition
)
@@ -75,38 +73,49 @@ open class UploadManagerFragment : Fragment() {
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
- mediaAdapter?.deleteItem(viewHolder.bindingAdapterPosition)
+ uploadMediaAdapter?.deleteItem(viewHolder.bindingAdapterPosition)
}
})
- mItemTouchHelper.attachToRecyclerView(mBinding.uploadList)
+ mItemTouchHelper.attachToRecyclerView(binding.uploadList)
- return mBinding.root
+ binding.root.findViewById(R.id.done_button)?.setOnClickListener {
+ dismiss() // Close the bottom sheet when clicked
+ }
+
+ return binding.root
}
override fun onResume() {
super.onResume()
-
refresh()
}
+ override fun onDestroy() {
+ super.onDestroy()
+
+ // Notify MainActivity that this fragment is dismissed
+ (activity as? MainActivity)?.uploadManagerFragment = null
+ }
+
open fun updateItem(mediaId: Long) {
- mediaAdapter?.updateItem(mediaId, -1)
+ uploadMediaAdapter?.updateItem(mediaId, -1)
}
open fun removeItem(mediaId: Long) {
- mediaAdapter?.removeItem(mediaId)
+ uploadMediaAdapter?.removeItem(mediaId)
}
fun setEditMode(isEditMode: Boolean) {
- mediaAdapter?.isEditMode = isEditMode
+ uploadMediaAdapter?.isEditMode = isEditMode
+ uploadMediaAdapter?.notifyDataSetChanged()
}
open fun refresh() {
- mediaAdapter?.updateData(Media.getByStatus(STATUSES, Media.ORDER_PRIORITY))
+ uploadMediaAdapter?.updateData(Media.getByStatus(STATUSES, Media.ORDER_PRIORITY))
}
open fun getUploadingCounter(): Int {
- return mediaAdapter?.media?.size ?: 0
+ return uploadMediaAdapter?.media?.size ?: 0
}
}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/Prefs.kt b/app/src/main/java/net/opendasharchive/openarchive/util/Prefs.kt
index 23e90f4b..0e45e658 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/util/Prefs.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/util/Prefs.kt
@@ -14,7 +14,7 @@ object Prefs {
const val UPLOAD_WIFI_ONLY = "upload_wifi_only"
private const val NEARBY_USE_BLUETOOTH = "nearby_use_bluetooth"
private const val NEARBY_USE_WIFI = "nearby_use_wifi"
- const val USE_TOR = "use_tor"
+ const val USE_TOR = "pref_use_tor"
const val PROHIBIT_SCREENSHOTS = "prohibit_screenshots"
const val USE_PROOFMODE = "use_proofmode"
const val USE_PROOFMODE_KEY_ENCRYPTION = "proofmode_key_encryption"
@@ -208,6 +208,7 @@ object Prefs {
val theme: Theme
get() = Theme.get(prefs?.getString(THEME, null))
+
var prohibitScreenshots: Boolean
get() = prefs?.getBoolean(PROHIBIT_SCREENSHOTS, false) ?: false
set(value) {
diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/Theme.kt b/app/src/main/java/net/opendasharchive/openarchive/util/Theme.kt
index ea053a39..905c1f84 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/util/Theme.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/util/Theme.kt
@@ -47,5 +47,13 @@ enum class Theme(val mode: Int) {
fun get(name: String?): Theme {
return entries.firstOrNull { it.name.uppercase() == name?.uppercase() } ?: SYSTEM
}
+
+ var darkModeEnabled: Boolean
+ get() {
+ return AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES
+ }
+ set(value) {
+ AppCompatDelegate.setDefaultNightMode(if (value) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
+ }
}
}
diff --git a/app/src/main/res/anim/popdown_anim.xml b/app/src/main/res/anim/popdown_anim.xml
new file mode 100644
index 00000000..1f4257fc
--- /dev/null
+++ b/app/src/main/res/anim/popdown_anim.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/popup_anim.xml b/app/src/main/res/anim/popup_anim.xml
new file mode 100644
index 00000000..fe5bd7d4
--- /dev/null
+++ b/app/src/main/res/anim/popup_anim.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_pill_white.xml b/app/src/main/res/drawable/bg_pill_white.xml
new file mode 100644
index 00000000..7a40163e
--- /dev/null
+++ b/app/src/main/res/drawable/bg_pill_white.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bottom_nav_background.xml b/app/src/main/res/drawable/bottom_nav_background.xml
index 9d289262..2ad3d6a7 100644
--- a/app/src/main/res/drawable/bottom_nav_background.xml
+++ b/app/src/main/res/drawable/bottom_nav_background.xml
@@ -4,10 +4,11 @@
-
-
+
- -
+
+
-
diff --git a/app/src/main/res/drawable/button.xml b/app/src/main/res/drawable/button.xml
index 1cdaaf9f..2aea7cc9 100644
--- a/app/src/main/res/drawable/button.xml
+++ b/app/src/main/res/drawable/button.xml
@@ -1,6 +1,19 @@
-
-
-
-
+
+
+
-
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml
index 436777fe..b9f38af4 100644
--- a/app/src/main/res/drawable/divider.xml
+++ b/app/src/main/res/drawable/divider.xml
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_back_ios.xml b/app/src/main/res/drawable/ic_arrow_back_ios.xml
new file mode 100644
index 00000000..d92cd9b0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_back_ios.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_left_ios.xml b/app/src/main/res/drawable/ic_arrow_left_ios.xml
new file mode 100644
index 00000000..35b73d1f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_left_ios.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_right_ios.xml b/app/src/main/res/drawable/ic_arrow_right_ios.xml
new file mode 100644
index 00000000..f824725a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_right_ios.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_submit.xml b/app/src/main/res/drawable/ic_arrow_submit.xml
new file mode 100644
index 00000000..757fa392
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_submit.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_browse_existing_folders.xml b/app/src/main/res/drawable/ic_browse_existing_folders.xml
new file mode 100644
index 00000000..3204babb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_browse_existing_folders.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml
new file mode 100644
index 00000000..eac31f02
--- /dev/null
+++ b/app/src/main/res/drawable/ic_close.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_create_new_folder.xml b/app/src/main/res/drawable/ic_create_new_folder.xml
new file mode 100644
index 00000000..21c73ca1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_create_new_folder.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_done.xml b/app/src/main/res/drawable/ic_done.xml
new file mode 100644
index 00000000..63ef9a07
--- /dev/null
+++ b/app/src/main/res/drawable/ic_done.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_dweb.xml b/app/src/main/res/drawable/ic_dweb.xml
new file mode 100644
index 00000000..0595d6e6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_dweb.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_edit_folder.xml b/app/src/main/res/drawable/ic_edit_folder.xml
new file mode 100644
index 00000000..b8cd6f91
--- /dev/null
+++ b/app/src/main/res/drawable/ic_edit_folder.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_folder_new.xml b/app/src/main/res/drawable/ic_folder_new.xml
new file mode 100644
index 00000000..ef8e622a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_folder_new.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_info_outline.xml b/app/src/main/res/drawable/ic_info_outline.xml
new file mode 100644
index 00000000..6f023cc0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_info_outline.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_pdf.xml b/app/src/main/res/drawable/ic_pdf.xml
new file mode 100644
index 00000000..4e1e8c66
--- /dev/null
+++ b/app/src/main/res/drawable/ic_pdf.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_space_dweb.xml b/app/src/main/res/drawable/ic_space_dweb.xml
new file mode 100644
index 00000000..f05269f9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_space_dweb.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_space_interent_archive.xml b/app/src/main/res/drawable/ic_space_interent_archive.xml
new file mode 100644
index 00000000..185d001c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_space_interent_archive.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_space_private_server.xml b/app/src/main/res/drawable/ic_space_private_server.xml
new file mode 100644
index 00000000..3caee61d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_space_private_server.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_trash.xml b/app/src/main/res/drawable/ic_trash.xml
new file mode 100644
index 00000000..e12c13c0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_trash.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_unknown_file.xml b/app/src/main/res/drawable/ic_unknown_file.xml
new file mode 100644
index 00000000..01ea8e9b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_unknown_file.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/list_item_background_selector.xml b/app/src/main/res/drawable/list_item_background_selector.xml
new file mode 100644
index 00000000..31ee803d
--- /dev/null
+++ b/app/src/main/res/drawable/list_item_background_selector.xml
@@ -0,0 +1,18 @@
+
+
+
+
+-
+
+
+
+
+
+
+-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/onboarding_archive_png.png b/app/src/main/res/drawable/onboarding_archive_png.png
new file mode 100644
index 00000000..c7a99be9
Binary files /dev/null and b/app/src/main/res/drawable/onboarding_archive_png.png differ
diff --git a/app/src/main/res/drawable/onboarding_encrypt_png.png b/app/src/main/res/drawable/onboarding_encrypt_png.png
new file mode 100644
index 00000000..d7adfe3d
Binary files /dev/null and b/app/src/main/res/drawable/onboarding_encrypt_png.png differ
diff --git a/app/src/main/res/drawable/onboarding_secure_png.png b/app/src/main/res/drawable/onboarding_secure_png.png
new file mode 100644
index 00000000..1927732f
Binary files /dev/null and b/app/src/main/res/drawable/onboarding_secure_png.png differ
diff --git a/app/src/main/res/drawable/onboarding_verify_png.png b/app/src/main/res/drawable/onboarding_verify_png.png
new file mode 100644
index 00000000..31e3d33a
Binary files /dev/null and b/app/src/main/res/drawable/onboarding_verify_png.png differ
diff --git a/app/src/main/res/drawable/rounded_bottom_sheet_background.xml b/app/src/main/res/drawable/rounded_bottom_sheet_background.xml
new file mode 100644
index 00000000..fb1d6ac5
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_bottom_sheet_background.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/uppload_adapter_custom_divider.xml b/app/src/main/res/drawable/uppload_adapter_custom_divider.xml
new file mode 100644
index 00000000..21131c9f
--- /dev/null
+++ b/app/src/main/res/drawable/uppload_adapter_custom_divider.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/welcome_arrow.xml b/app/src/main/res/drawable/welcome_arrow.xml
index a1a3c193..6a071c26 100644
--- a/app/src/main/res/drawable/welcome_arrow.xml
+++ b/app/src/main/res/drawable/welcome_arrow.xml
@@ -5,5 +5,5 @@
android:viewportHeight="279">
+ android:fillColor="@color/c23_medium_grey"/>
diff --git a/app/src/main/res/layout-land/fragment_onboarding23_slide.xml b/app/src/main/res/layout-land/fragment_onboarding23_slide.xml
index beaf9fc6..f73faf98 100644
--- a/app/src/main/res/layout-land/fragment_onboarding23_slide.xml
+++ b/app/src/main/res/layout-land/fragment_onboarding23_slide.xml
@@ -39,7 +39,7 @@
android:textSize="28sp"
android:layout_above="@id/summary"
tools:ignore="HardcodedText"
- tools:targetApi="p" />
+ />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_folders.xml b/app/src/main/res/layout/activity_folders.xml
index a9e7ae59..f08c997b 100644
--- a/app/src/main/res/layout/activity_folders.xml
+++ b/app/src/main/res/layout/activity_folders.xml
@@ -25,20 +25,18 @@
android:layout_marginHorizontal="8dp"
android:id="@+id/rv_projects"
android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_vertical_margin" />
-
+
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 33734659..50b5a97b 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,334 +1,156 @@
-
-
-
-
+
+
-
-
-
-
-
-
-
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
-
+
+
-
+ android:layout_gravity="end"
+ android:clipChildren="true"
+ android:fitsSystemWindows="true">
-
-
-
-
+
+
-
-
-
-
-
-
+ android:orientation="horizontal"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
-
+ android:id="@+id/current_space_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ tools:src="@drawable/ic_internet_archive" />
-
-
-
-
+ android:layout_marginHorizontal="12dp"
+ android:textSize="16sp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Upul's Server" />
-
-
-
-
+
+
+
+
+
+
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/navigation_drawer_header"
+ tools:listitem="@layout/rv_drawer_row" />
+
-
-
-
-
+ app:layout_constraintStart_toStartOf="parent" />
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
+
+
diff --git a/app/src/main/res/layout/activity_onboarding23.xml b/app/src/main/res/layout/activity_onboarding23.xml
index 29eb9e55..e6c12f1a 100644
--- a/app/src/main/res/layout/activity_onboarding23.xml
+++ b/app/src/main/res/layout/activity_onboarding23.xml
@@ -43,20 +43,22 @@
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="vertical"
- android:paddingLeft="20dp"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
android:paddingRight="20dp"
android:paddingBottom="10dp">
+
+
diff --git a/app/src/main/res/layout/activity_onboarding23_instructions.xml b/app/src/main/res/layout/activity_onboarding23_instructions.xml
index 931e6890..0e55b8dd 100644
--- a/app/src/main/res/layout/activity_onboarding23_instructions.xml
+++ b/app/src/main/res/layout/activity_onboarding23_instructions.xml
@@ -17,7 +17,7 @@
android:layout_gravity="end"
android:background="@color/colorTertiary">
-
+ android:textFontWeight="600"
+ android:textColor="@color/colorOnBackground" />
+ app:tint="@color/colorOnBackground" />
-
+ app:layout_constraintStart_toStartOf="parent">
+
+
+
+
+
+
+
+ app:layout_constraintVertical_weight="0.55"
+ tools:visibility="gone">
-
+
+
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/no_thumbnail" />
-
-
-
+
-
+
-
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_max="320dp"
+ app:srcCompat="@drawable/no_thumbnail">
-
-
+
+
+
+
+
+
+
+
-
+
-
+
+
+
@@ -131,52 +174,56 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
-
+
-
-
+
+
+
+
+
+
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintVertical_weight="0.45">
+ app:layout_constraintTop_toTopOf="parent">
+ app:layout_constraintStart_toStartOf="parent">
+ android:inputType="textMultiLine"
+ android:minLines="3" />
diff --git a/app/src/main/res/layout/activity_settings_container.xml b/app/src/main/res/layout/activity_settings_container.xml
index bb09b3f2..54840a67 100644
--- a/app/src/main/res/layout/activity_settings_container.xml
+++ b/app/src/main/res/layout/activity_settings_container.xml
@@ -1,5 +1,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_space_setup.xml b/app/src/main/res/layout/activity_space_setup.xml
index 887bcb25..a2dbfc7f 100644
--- a/app/src/main/res/layout/activity_space_setup.xml
+++ b/app/src/main/res/layout/activity_space_setup.xml
@@ -1,30 +1,29 @@
-
-
+ android:layout_height="wrap_content" />
-
-
+
+
+
-
-
diff --git a/app/src/main/res/layout/activity_spaces.xml b/app/src/main/res/layout/activity_spaces.xml
deleted file mode 100644
index 91e4bb88..00000000
--- a/app/src/main/res/layout/activity_spaces.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upload_manager.xml b/app/src/main/res/layout/activity_upload_manager.xml
index c079cba8..cce55e17 100644
--- a/app/src/main/res/layout/activity_upload_manager.xml
+++ b/app/src/main/res/layout/activity_upload_manager.xml
@@ -9,7 +9,13 @@
layout="@layout/common_app_bar" />
-
+
+
+
diff --git a/app/src/main/res/layout/activity_webdav.xml b/app/src/main/res/layout/activity_webdav.xml
index 14efe68a..15d46d7f 100644
--- a/app/src/main/res/layout/activity_webdav.xml
+++ b/app/src/main/res/layout/activity_webdav.xml
@@ -15,6 +15,17 @@
layout="@layout/common_app_bar" />
+
+
@@ -50,7 +50,7 @@
android:textFontWeight="900"
android:textStyle="bold"
app:autoSizeTextType="uniform"
- tools:targetApi="p" />
+ />
@@ -78,7 +78,7 @@
android:textFontWeight="900"
android:textStyle="bold"
app:autoSizeTextType="uniform"
- tools:targetApi="p" />
+ />
+ />
@@ -134,7 +134,7 @@
android:textFontWeight="900"
android:textStyle="bold"
app:autoSizeTextType="uniform"
- tools:targetApi="p" />
+ />
@@ -146,11 +146,11 @@
diff --git a/app/src/main/res/layout/common_app_bar.xml b/app/src/main/res/layout/common_app_bar.xml
index 2e31c5ec..c38de804 100644
--- a/app/src/main/res/layout/common_app_bar.xml
+++ b/app/src/main/res/layout/common_app_bar.xml
@@ -8,7 +8,6 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/custom_bottom_nav.xml b/app/src/main/res/layout/custom_bottom_nav.xml
index aad88027..6dd83289 100644
--- a/app/src/main/res/layout/custom_bottom_nav.xml
+++ b/app/src/main/res/layout/custom_bottom_nav.xml
@@ -56,8 +56,8 @@
-
diff --git a/app/src/main/res/layout/custom_preference_category.xml b/app/src/main/res/layout/custom_preference_category.xml
index 98e7e46b..7044afa2 100644
--- a/app/src/main/res/layout/custom_preference_category.xml
+++ b/app/src/main/res/layout/custom_preference_category.xml
@@ -6,7 +6,7 @@
android:background="@android:color/transparent"
android:orientation="vertical"
android:paddingHorizontal="16dp"
- android:paddingTop="16dp"
+ android:paddingTop="24dp"
android:paddingBottom="8dp">
@@ -17,7 +17,7 @@
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:lineSpacingExtra="3sp"
- android:textColor="@color/c23_teal"
+ android:textColor="@color/colorTertiary"
android:textFontWeight="600"
android:textSize="18sp"
android:textStyle="normal"
diff --git a/app/src/main/res/layout/custom_preference_divider.xml b/app/src/main/res/layout/custom_preference_divider.xml
index 7fb2f325..608af893 100644
--- a/app/src/main/res/layout/custom_preference_divider.xml
+++ b/app/src/main/res/layout/custom_preference_divider.xml
@@ -2,5 +2,5 @@
\ No newline at end of file
diff --git a/app/src/main/res/layout/custom_preference_switch.xml b/app/src/main/res/layout/custom_preference_switch.xml
index 9ccd37f3..103a0416 100644
--- a/app/src/main/res/layout/custom_preference_switch.xml
+++ b/app/src/main/res/layout/custom_preference_switch.xml
@@ -20,6 +20,7 @@
@@ -30,30 +31,29 @@
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
+ android:textColor="@color/colorOnBackground"
android:textFontWeight="500"
- android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="14sp"
tools:text="Title" />
-
-
-
+ android:orientation="vertical" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/folder_row.xml b/app/src/main/res/layout/folder_row.xml
index 53bf25f5..c49828b7 100644
--- a/app/src/main/res/layout/folder_row.xml
+++ b/app/src/main/res/layout/folder_row.xml
@@ -6,7 +6,7 @@
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:padding="8dp"
- android:background="@drawable/item_background_selector"
+ android:background="@drawable/list_item_background_selector"
android:filterTouchesWhenObscured="true">
@@ -30,7 +30,7 @@
android:layout_marginEnd="@dimen/activity_vertical_margin"
android:importantForAccessibility="no"
app:tint="@color/colorOnPrimaryContainer"
- android:src="@drawable/ic_folder" />
+ android:src="@drawable/ic_folder_new" />
+ app:tint="@color/colorOnBackground" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_add_media_dialog.xml b/app/src/main/res/layout/fragment_add_media_dialog.xml
index eedd20bb..8f6b50d0 100644
--- a/app/src/main/res/layout/fragment_add_media_dialog.xml
+++ b/app/src/main/res/layout/fragment_add_media_dialog.xml
@@ -185,7 +185,7 @@
android:textFontWeight="700"
android:textSize="14sp"
app:layout_constraintWidth_percent="0.8"
- tools:targetApi="p" />
+ />
diff --git a/app/src/main/res/layout/activity_browse_folders.xml b/app/src/main/res/layout/fragment_browse_folders.xml
similarity index 92%
rename from app/src/main/res/layout/activity_browse_folders.xml
rename to app/src/main/res/layout/fragment_browse_folders.xml
index 512ede21..01c7aba8 100644
--- a/app/src/main/res/layout/activity_browse_folders.xml
+++ b/app/src/main/res/layout/fragment_browse_folders.xml
@@ -6,17 +6,13 @@
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true"
tools:background="@color/colorBackground"
- tools:context=".features.folders.BrowseFoldersActivity">
+ tools:context=".features.folders.BrowseFoldersFragment">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_main_media.xml b/app/src/main/res/layout/fragment_main_media.xml
index aa90f9fb..312d532d 100644
--- a/app/src/main/res/layout/fragment_main_media.xml
+++ b/app/src/main/res/layout/fragment_main_media.xml
@@ -15,31 +15,24 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="top"
- android:layout_marginTop="6dp"
android:gravity="top"
android:orientation="vertical">
-
-
-
+ android:layout_height="match_parent"
+ android:elevation="1dp">
+ app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toBottomOf="@id/tv_welcome" />
-
+ app:tint="@color/c23_medium_grey">
+
+
+
diff --git a/app/src/main/res/layout/fragment_onboarding23_slide.xml b/app/src/main/res/layout/fragment_onboarding23_slide.xml
index a36cd1ad..bc032b37 100644
--- a/app/src/main/res/layout/fragment_onboarding23_slide.xml
+++ b/app/src/main/res/layout/fragment_onboarding23_slide.xml
@@ -37,13 +37,16 @@
android:paddingBottom="8dp"
android:text="title"
android:textAllCaps="true"
- android:textFontWeight="900"
+ android:textFontWeight="800"
android:textSize="28sp"
android:textStyle="bold"
tools:ignore="HardcodedText" />
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_space_setup.xml b/app/src/main/res/layout/fragment_space_setup.xml
index c3f28f0f..938edd8b 100644
--- a/app/src/main/res/layout/fragment_space_setup.xml
+++ b/app/src/main/res/layout/fragment_space_setup.xml
@@ -1,326 +1,347 @@
-
-
+
+
+
+ android:orientation="vertical">
+
-
-
-
-
-
-
-
-
-
+ android:padding="8dp"
+ android:visibility="gone">
+ android:paddingVertical="8dp">
+ android:layout_gravity="center_horizontal"
+ android:layout_marginHorizontal="@dimen/activity_horizontal_margin"
+ android:layout_marginBottom="@dimen/activity_vertical_margin"
+ android:gravity="center"
+ android:text="@string/to_get_started_connect_to_a_server_to_store_your_media"
+ android:textSize="18sp"
+ android:textStyle="bold" />
-
+ android:layout_gravity="center_horizontal"
+ android:layout_marginVertical="6dp"
+ android:gravity="center_horizontal"
+ android:text="@string/to_get_started_more_hint"
+ android:textSize="14sp" />
-
-
-
+ android:layout_gravity="center_vertical"
+ android:layout_marginHorizontal="@dimen/activity_horizontal_margin"
+ android:layout_marginVertical="16dp"
+ android:background="@drawable/item_background_selector"
+ android:orientation="horizontal"
+ android:padding="4dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+ android:importantForAccessibility="no"
+ android:src="@drawable/keyboard_arrow_right"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/internet_archive_texts"
+ app:layout_constraintTop_toTopOf="parent"
+ app:tint="@color/colorText" />
+
-
-
-
-
-
-
+
+
+
+
+ android:orientation="vertical"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/icon_next_snowbird"
+ app:layout_constraintStart_toEndOf="@id/icon_snowbird"
+ app:layout_constraintTop_toTopOf="parent">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_space_setup_success.xml b/app/src/main/res/layout/fragment_space_setup_success.xml
index a8bb80a1..8db23332 100644
--- a/app/src/main/res/layout/fragment_space_setup_success.xml
+++ b/app/src/main/res/layout/fragment_space_setup_success.xml
@@ -11,13 +11,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/activity_horizontal_margin"
- android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin">
@@ -40,8 +41,8 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:importantForAccessibility="no"
+ android:scaleType="centerInside"
android:src="@drawable/hands_mobile_updated"
- android:paddingVertical="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
@@ -49,19 +50,35 @@
-
+ android:layout_width="match_parent"
+ android:weightSum="5"
+ android:orientation="horizontal"
+ android:gravity="center_horizontal"
+ android:layout_height="wrap_content">
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_upload_manager.xml b/app/src/main/res/layout/fragment_upload_manager.xml
index af691fa4..72178ef8 100644
--- a/app/src/main/res/layout/fragment_upload_manager.xml
+++ b/app/src/main/res/layout/fragment_upload_manager.xml
@@ -1,23 +1,84 @@
-
+ android:fillViewport="true"
+ app:layout_behavior="@string/bottom_sheet_behavior">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_height="match_parent"
+ android:nestedScrollingEnabled="false" />
-
+
diff --git a/app/src/main/res/layout/fragment_web_dav.xml b/app/src/main/res/layout/fragment_web_dav.xml
index 8a1cac9a..00503849 100644
--- a/app/src/main/res/layout/fragment_web_dav.xml
+++ b/app/src/main/res/layout/fragment_web_dav.xml
@@ -35,7 +35,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
- android:layout_marginVertical="24dp">
+ android:layout_marginTop="48dp"
+ android:layout_marginBottom="24dp">
@@ -62,9 +64,10 @@
android:textFontWeight="700"
android:textStyle="bold"
android:visibility="gone"
- tools:targetApi="p" />
+ />
@@ -83,29 +86,35 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
- android:text="@string/server_info" />
+ android:text="@string/server_info"
+ android:textFontWeight="600"
+ android:textSize="18sp" />
+ android:layout_marginBottom="8dp"
+ app:hintEnabled="false"
+ android:paddingVertical="4dp">
+ android:visibility="visible"
+ tools:visibility="visible">
+ android:text="@string/account"
+ android:textFontWeight="600"
+ android:textSize="18sp" />
+ android:layout_marginBottom="8dp"
+ android:paddingVertical="4dp">
+
-
-
-
+
-
+
+
-
-
-
-
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:visibility="visible">
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+ app:layout_constraintStart_toStartOf="parent"
+ tools:visibility="gone">
+ android:layout_marginHorizontal="24dp"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:textColor="@color/colorText">
+ android:text="@string/back"
+ android:textColor="@color/colorText" />
+
diff --git a/app/src/main/res/layout/fragment_webdav_setup_license.xml b/app/src/main/res/layout/fragment_webdav_setup_license.xml
index 73521a9b..876a492a 100644
--- a/app/src/main/res/layout/fragment_webdav_setup_license.xml
+++ b/app/src/main/res/layout/fragment_webdav_setup_license.xml
@@ -11,11 +11,12 @@
+ android:textColor="@color/colorText"
+ android:visibility="visible" />
@@ -98,13 +99,12 @@
tools:ignore="ButtonStyle">
diff --git a/app/src/main/res/layout/popup_folder_options.xml b/app/src/main/res/layout/popup_folder_options.xml
new file mode 100644
index 00000000..0e53ef94
--- /dev/null
+++ b/app/src/main/res/layout/popup_folder_options.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/rv_drawer_row.xml b/app/src/main/res/layout/rv_drawer_row.xml
new file mode 100644
index 00000000..bd2c0400
--- /dev/null
+++ b/app/src/main/res/layout/rv_drawer_row.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/rv_folders_row.xml b/app/src/main/res/layout/rv_folders_row.xml
index d8792170..607256a5 100644
--- a/app/src/main/res/layout/rv_folders_row.xml
+++ b/app/src/main/res/layout/rv_folders_row.xml
@@ -1,70 +1,32 @@
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_gravity="center_vertical"
+ android:layout_marginHorizontal="10dp"
+ android:gravity="center_vertical"
+ android:textSize="18sp" />
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/rv_media_box.xml b/app/src/main/res/layout/rv_media_box.xml
index cd7332e2..44d07162 100644
--- a/app/src/main/res/layout/rv_media_box.xml
+++ b/app/src/main/res/layout/rv_media_box.xml
@@ -40,24 +40,26 @@
android:id="@+id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@color/transparent_black">
+ android:background="@color/transparent_loading_overlay">
+ app:trackThickness="4dp" />
+
+
+
+
+
+ app:trackThickness="4dp" />
-
-
-
+ app:layout_constraintTop_toTopOf="parent">
+
+
+
+
+
+
+
+
+ app:srcCompat="@drawable/ic_reorder_black_24dp" />
diff --git a/app/src/main/res/layout/view_section.xml b/app/src/main/res/layout/view_section.xml
index c85cc961..7c21a396 100644
--- a/app/src/main/res/layout/view_section.xml
+++ b/app/src/main/res/layout/view_section.xml
@@ -25,7 +25,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
- android:textSize="12sp"
+ android:textSize="11sp"
+ android:textColor="@color/colorOnBackground"
+ android:textFontWeight="500"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@@ -38,6 +40,9 @@
android:layout_marginStart="8dp"
android:background="@drawable/pill"
android:gravity="center"
+ android:textSize="11sp"
+ android:textFontWeight="500"
+ android:textColor="@color/colorOnBackground"
android:paddingStart="8dp"
android:paddingEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
diff --git a/app/src/main/res/menu/menu_confirm.xml b/app/src/main/res/menu/menu_confirm.xml
new file mode 100644
index 00000000..0ce6df0a
--- /dev/null
+++ b/app/src/main/res/menu/menu_confirm.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index 69d2baf8..4ee2cbe6 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -6,7 +6,7 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_main_edit_folder_bar.xml b/app/src/main/res/menu/menu_main_edit_folder_bar.xml
new file mode 100644
index 00000000..a08f4666
--- /dev/null
+++ b/app/src/main/res/menu/menu_main_edit_folder_bar.xml
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_upload.xml b/app/src/main/res/menu/menu_upload.xml
index 5ef3f9cf..b13726dd 100644
--- a/app/src/main/res/menu/menu_upload.xml
+++ b/app/src/main/res/menu/menu_upload.xml
@@ -5,9 +5,9 @@
tools:context=".features.main.MainActivity">
diff --git a/app/src/main/res/navigation/space_setup_navigation.xml b/app/src/main/res/navigation/space_setup_navigation.xml
new file mode 100644
index 00000000..a105f47a
--- /dev/null
+++ b/app/src/main/res/navigation/space_setup_navigation.xml
@@ -0,0 +1,296 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 160d3f04..4131da6d 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -19,7 +19,7 @@
@color/c23_teal_30
@color/c23_teal_60
- #16161D
+ #24242E
@color/c23_light_grey
@color/c23_teal_60
@color/c23_light_grey
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index f1930b30..099f8225 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -4,10 +4,12 @@
#ff0000
+ #DC341E
#00ff00
#0000ff
#000000
#33333333
+ #AB2B2B2B
#CCFFFFFF
#27aae1
#b0e7ff
@@ -17,6 +19,7 @@
#9e9e9e
#757575
#424242
+ #434343
#212121
#A92E33
@@ -74,7 +77,7 @@
- #F5F1E7
+ @color/white
@color/black
@color/c23_grey
@color/c23_grey
@@ -96,8 +99,9 @@
#F5F1E7
@color/c23_dark_grey
+ @color/text_grey
- @color/c23_teal_50
+ @color/c23_teal
@color/c23_dark_grey
@color/red
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index ee944980..c080b64b 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -1,6 +1,6 @@
- 16dp
+ 8dp
16dp
8dp
@@ -32,4 +32,7 @@
20sp
32dp
+
+ 280dp
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bf583344..fc416336 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -25,6 +25,8 @@
Internet Archive
Upload your media to a free public or paid private account on the Internet Archive.
+ DWeb Service
+ Connect to the decentralized web
Google Drive
Google Drive™
Upload to Google Drive
@@ -37,6 +39,10 @@
+
+ Disable Passcode
+ Are you sure you want to disable the passcode?
+
Main Image
@@ -45,6 +51,7 @@
Cancel
OK
+ Confirm
E-mail Confirmation
Please confirm your e-mail address and return to connect your Internet Archive account.
@@ -54,11 +61,14 @@
Allow commercial uses of your work?
-->
+
+ License
Allow anyone to remix and share?
Require them to share like you have?
Allow commercial use?
Remove Media
+ This media will be removed from Save.\nIt will remain on the server and in your Photos app.
Tap to flag as significant content
Yes
@@ -85,7 +95,13 @@
dark
Enable ProofMode
- Capture extra metadata and notarize all media.
+ Share ProofMode public key
+
+ learn more here
+ ]]>
+
+ ProofMode gathers metadata from local cell towers to help verify media. Android requires permission to enable this setting. Save will only use this setting to capture data and will NOT access your phone to make/manage calls.
ProofMode Identity
Share ProofMode Public Key
@@ -108,7 +124,7 @@
Importing media…
- Welcome
+ Welcome!
Privacy Policy
About %s
@@ -118,6 +134,8 @@
Upload Manager
Uploads
+ Edit Queue
+ Uploading is paused
Uploading…
Uploading… (%d left)
These notifications show up while this app is performing media uploads.
@@ -126,12 +144,14 @@
- Tap the button below to add media.
+ Tap the button below\nto add media
To get started, connect to a server to store your media.
+ You can add multiple private servers and\none IA or DWeb server account at any time.
+ You can add multiple private servers and\none IA account at any time.
In the side menu, you can add another server and connect to multiple servers
- Send to a webdav server
- Upload to the IA
- Save only connects to WebDAV-compatible servers, e.g Nextcloud and ownCloud.
+ Connect to a secure WebDAV server
+ Connect to a free public or paid private server
+ Connect to a WebDAV-compatible servers, e.g. Nexcloud and ownCloud.
Next
Done
Significant Content
@@ -141,9 +161,12 @@
You have successfully connected to the Internet Archive!
You have successfully connected to Google Drive!
Create a New Folder
+ First, please name your folder
+ This folder will be created on your server and automatically added on Save
+ Enter folder name
Edit
Done
- Remove from App
+ Remove from app
Are you sure you want to remove your project?
Remove
Archive Project
@@ -153,13 +176,14 @@
Secret Key
Save needs your Internet Archive account\'s API keys to be able to upload media.
Acquire Keys
- Add Folder
+ New Folder
Please do not include special characters in the name.
Are you sure you want to remove this server from the app?
Folder name already exists.
Dropbox
Metadata
+ Remove Media
Are you sure you want to remove this Media?
Flag Significant Content
@@ -182,7 +206,7 @@
Server Name (Optional)
Dropbox ID:
- Add Server
+ Add new server
Secure ProofMode Key with Biometrics or Device Passcode
Secure ProofMode Key with Biometrics
Secure ProofMode Key with Device Passcode
@@ -201,7 +225,7 @@
Back
My Media
If you do not have existing keys, learn how to acquire keys.
- Server Info
+ Server info
Enter URL
Account
General
@@ -211,10 +235,10 @@
ProofMode
No application can handle this request. Please install a webbrowser.
- SECURE
- ARCHIVE
- VERIFY
- ENCRYPT
+ Secure
+ Archive
+ Verify
+ Encrypt
Send your media securely to private servers and lock the app with a pin.
@@ -256,7 +280,7 @@
is video
Use Upload Chunking (Nextcloud Only)
\"Chunking\" uploads media in pieces so you don\'t have to restart your upload if your connection is interrupted.
- Preview Media
+ Preview Upload
Add More
Select All
Got it
@@ -322,6 +346,8 @@
Create one
No account?
Continue
+
+
Hello blank fragment
@@ -341,9 +367,24 @@
pref_media_servers
pref_media_folders
- passcode_enabled
+
Secure
Archive
+ Read our Terms & Privacy Policy
+ Media Servers
+ Media Folders
+ Manage your servers
+ Manage your folders
+ Read our Terms & Privacy Policy
+
+ passcode_enabled
+ pref_proof_mode
+ pref_use_proofmode
+ pref_use_tor
+ pref_is_dark_mode
+ pref_about_app
+ pref_privacy_policy
+ pref_app_version
@@ -354,4 +395,5 @@
You have added a folder successfully
+ You have changed your server settings successfully.
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index da5d26c8..f51d49be 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -2,17 +2,9 @@
@@ -26,9 +18,20 @@
- @style/OATextViewStyle
+ - @style/TextAppearance.SaveApp.18pt
+
+ - @style/TextAppearance.SaveApp.16pt
+ - @style/TextAppearance.SaveApp.14pt
+ - @style/TextAppearance.SaveApp.11pt
+ - @style/TextAppearance.Material3.BodySmall
+
+ - @style/TextAppearance.App.TitleLarge
+ - @style/TextAppearance.App.TitleMedium
+
+ - true
- @color/colorPrimary
- - @bool/dayMode
+ - false
- @bool/dayMode
- @style/NavigationViewStyle
- @style/AppDialogTheme
@@ -49,7 +52,7 @@
- @style/Widget.App.Button.TextButton
- @style/Widget.App.Button.OutlinedButton
-
+
- @style/TextAppearance.App.Button
@@ -57,24 +60,25 @@
- @color/white
- - @style/Widget.SaveApp.TextInputLayout
+ - @style/OATextInputLayoutStyle
- - @style/TextAppearance.App.TitleLarge
- - @style/TextAppearance.App.TitleMedium
- - @style/TextAppearance.App.BodySmall
- @style/ShapeAppearance.SaveApp.SmallComponent
- @style/Widget.SaveApp.Switch
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
-
+
+
diff --git a/app/src/main/res/xml/prefs_general.xml b/app/src/main/res/xml/prefs_general.xml
index f4d93a7a..11c24600 100644
--- a/app/src/main/res/xml/prefs_general.xml
+++ b/app/src/main/res/xml/prefs_general.xml
@@ -13,11 +13,12 @@
android:icon="@null"
android:iconSpaceReserved="false"
android:key="@string/pref_app_passcode"
+ android:layout="@layout/custom_preference_switch"
android:singleLineTitle="false"
android:title="Lock app with passcode"
+ android:widgetLayout="@layout/custom_preference_widget_switch"
app:icon="@null"
- app:iconSpaceReserved="false"
- app:summary="6 digit passcode" />
+ app:iconSpaceReserved="false" />
@@ -26,22 +27,35 @@
android:title="@string/pref_title_archive"
app:allowDividerAbove="true">
+
+
@@ -68,7 +82,8 @@
@@ -82,10 +97,12 @@
@@ -96,39 +113,37 @@
android:layout="@layout/custom_preference_category"
android:title="General">
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+ android:key="@string/pref_key_use_dark_mode"
+ android:layout="@layout/custom_preference_switch"
+ android:singleLineTitle="true"
+ android:title="Switch to dark mode"
+ android:widgetLayout="@layout/custom_preference_widget_switch"
+ app:icon="@null"
+ app:iconSpaceReserved="false" />
@@ -139,10 +154,11 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/prefs_proof_mode.xml b/app/src/main/res/xml/prefs_proof_mode.xml
index 55e042bb..bd332a04 100644
--- a/app/src/main/res/xml/prefs_proof_mode.xml
+++ b/app/src/main/res/xml/prefs_proof_mode.xml
@@ -1,36 +1,16 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ app:key="@string/pref_key_use_proof_mode"
+ app:singleLineTitle="false"
+ app:summary="@string/prefs_use_proofmode_summary"
+ app:title="@string/prefs_use_proofmode_title" />
\ No newline at end of file
diff --git a/app/src/test/java/net/opendasharchive/openarchive/MainMediaAdapterTest.kt b/app/src/test/java/net/opendasharchive/openarchive/MainMediaAdapterTest.kt
new file mode 100644
index 00000000..9600621a
--- /dev/null
+++ b/app/src/test/java/net/opendasharchive/openarchive/MainMediaAdapterTest.kt
@@ -0,0 +1,186 @@
+package net.opendasharchive.openarchive
+
+import android.app.Activity
+import androidx.recyclerview.widget.RecyclerView
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import net.opendasharchive.openarchive.db.Media
+import net.opendasharchive.openarchive.features.main.adapters.MainMediaAdapter
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import java.util.Date
+
+
+// Since Media is a SugarRecord and calls save()/delete() internally,
+// for testing we want to stub these methods.
+// One option is to use a fake subclass. For simplicity, we’ll create a helper
+// function that creates Media instances with test values. (In a real test suite,
+// you may want to use a mocking library like Mockito or create a fake subclass.)
+fun createTestMedia(
+ id: Long,
+ uri: String,
+ status: Media.Status,
+ progress: Int? = 0,
+ selected: Boolean = false,
+ title: String = "Test Media"
+): Media {
+ return Media(
+ originalFilePath = uri,
+ mimeType = "image/jpeg",
+ createDate = Date(),
+ title = title,
+ description = "",
+ author = "",
+ location = "",
+ tags = "",
+ licenseUrl = null,
+ mediaHash = byteArrayOf(),
+ mediaHashString = "",
+ status = status.id,
+ statusMessage = "",
+ projectId = 100,
+ collectionId = 200,
+ contentLength = 1024L,
+ progress = 0,
+ flag = false,
+ priority = 0,
+ selected = selected
+ ).apply {
+ // Stub out save() and delete() if needed (SugarRecord normally writes to DB).
+ // For tests, you can override these to no-ops.
+ // For example, if using Mockito, you could spy() and stub save()/delete().
+ }
+}
+
+@RunWith(RobolectricTestRunner::class)
+class MainMediaAdapterTest {
+
+
+ private lateinit var activity: Activity
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var adapter: MainMediaAdapter
+ private lateinit var mediaList: MutableList
+
+ @Before
+ fun setup() {
+ // Use Robolectric to create an Activity
+ activity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ recyclerView = RecyclerView(activity)
+ // Create a dummy list of Media items
+ mediaList = mutableListOf(
+ createTestMedia(id = 1, uri = "file://uri1", status = Media.Status.Local, progress = 0),
+ createTestMedia(
+ id = 2,
+ uri = "file://uri2",
+ status = Media.Status.Queued,
+ progress = 0
+ ),
+ createTestMedia(
+ id = 3,
+ uri = "file://uri3",
+ status = Media.Status.Uploading,
+ progress = 50
+ )
+ )
+ adapter = MainMediaAdapter(
+ activity,
+ mediaList,
+ recyclerView,
+ checkSelecting = { },
+ )
+ }
+
+ @Test
+ fun testItemCountAndGetItemId() {
+ // Verify that the item count matches the list size.
+ assertEquals(mediaList.size, adapter.itemCount)
+ // Verify stable IDs are returned correctly.
+ assertEquals(1L, adapter.getItemId(0))
+ assertEquals(2L, adapter.getItemId(1))
+ assertEquals(3L, adapter.getItemId(2))
+ }
+
+ @Test
+ fun testUpdateItemProgress() {
+ // Update item with id 3 to a new progress value.
+ val result = adapter.updateItem(3, progress = 80)
+ assertTrue(result)
+ val updatedMedia = adapter.media.first { it.id == 3L }
+ assertEquals(80, updatedMedia.uploadPercentage)
+ // Since we update the status when progress is updated, check that status is set to Uploading.
+ assertEquals(Media.Status.Uploading.id, updatedMedia.status)
+ }
+
+ @Test
+ fun testRemoveItem() {
+ // Remove media with id 2.
+ val result = adapter.removeItem(2)
+ assertTrue(result)
+ // After removal, item count should decrease.
+ assertEquals(2, adapter.itemCount)
+ // And no item with id 2 should exist.
+ assertFalse(adapter.media.any { it.id == 2L })
+ }
+
+ @Test
+ fun testUpdateData() {
+ // Simulate a data refresh with a new media list.
+ val newMediaList = listOf(
+ createTestMedia(
+ id = 1,
+ uri = "file://uri1",
+ status = Media.Status.Uploaded,
+ progress = 100
+ ),
+ createTestMedia(id = 2, uri = "file://uri2", status = Media.Status.Local, progress = 0),
+ createTestMedia(id = 4, uri = "file://uri4", status = Media.Status.Local, progress = 0)
+ )
+ adapter.updateData(newMediaList)
+ // Verify that the adapter now has three items.
+ assertEquals(3, adapter.itemCount)
+ // New item (id = 4) should be present.
+ assertTrue(adapter.media.any { it.id == 4L })
+ // Item with id 3 should have been removed.
+ assertFalse(adapter.media.any { it.id == 3L })
+ }
+
+ @Test
+ fun testClearSelections() {
+ // Mark some items as selected.
+ adapter.media[0].selected = true
+ adapter.media[1].selected = true
+ // Call clearSelections() and verify all items are unselected.
+ adapter.clearSelections()
+ adapter.media.forEach { assertFalse(it.selected) }
+ }
+
+ @Test
+ fun testOnItemMove() {
+ // Enable edit mode so onItemMove works.
+ adapter.isEditMode = true
+ val firstItemId = adapter.media[0].id
+ // Move item at position 0 to position 2.
+ adapter.onItemMove(0, 2)
+ // Check that the item now appears at position 2.
+ assertEquals(firstItemId, adapter.media[2].id)
+ }
+
+ @Test
+ fun testDeleteSelected() {
+ // Mark two items as selected.
+ adapter.media[0].selected = true
+ adapter.media[2].selected = true
+ val originalCount = adapter.itemCount
+ // Call deleteSelected() and verify it returns true.
+ val result = adapter.deleteSelected()
+ assertTrue(result)
+ // The new count should be originalCount - 2.
+ assertEquals(originalCount - 2, adapter.itemCount)
+ // Verify that no selected items remain.
+ assertEquals(0, adapter.getSelectedCount())
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 6befae92..27898215 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,15 +1,13 @@
plugins {
- id("com.android.application") version "8.6.1" apply false
+ id("com.android.application") version "8.3.2" apply false
id("org.jetbrains.kotlin.android") version "2.1.10" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.1.10" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10" apply false
- id("com.google.devtools.ksp") version "2.1.10-1.0.29" apply false
-}
+ id("com.google.devtools.ksp") version "2.1.10-1.0.30" apply false
+
+ id("androidx.navigation.safeargs") version "2.8.8" apply false
-configurations.configureEach {
- resolutionStrategy {
- force("com.android.support:support-v4:1.0.0")
- }
+ alias(libs.plugins.detekt.plugin) apply false
}
tasks.register("clean") {
diff --git a/config/detekt-config.yml b/config/detekt-config.yml
new file mode 100644
index 00000000..a99959f9
--- /dev/null
+++ b/config/detekt-config.yml
@@ -0,0 +1,797 @@
+build:
+ maxIssues: 0
+ excludeCorrectable: false
+ weights:
+ # complexity: 2
+ # LongParameterList: 1
+ # style: 1
+ # comments: 1
+
+config:
+ validation: true
+ warningsAsErrors: false
+ checkExhaustiveness: false
+ # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
+ excludes: ''
+
+processors:
+ active: true
+ exclude:
+ - 'DetektProgressListener'
+ # - 'KtFileCountProcessor'
+ # - 'PackageCountProcessor'
+ # - 'ClassCountProcessor'
+ # - 'FunctionCountProcessor'
+ # - 'PropertyCountProcessor'
+ # - 'ProjectComplexityProcessor'
+ # - 'ProjectCognitiveComplexityProcessor'
+ # - 'ProjectLLOCProcessor'
+ # - 'ProjectCLOCProcessor'
+ # - 'ProjectLOCProcessor'
+ # - 'ProjectSLOCProcessor'
+ # - 'LicenseHeaderLoaderExtension'
+
+console-reports:
+ active: true
+ exclude:
+ - 'ProjectStatisticsReport'
+ - 'ComplexityReport'
+ - 'NotificationReport'
+ - 'FindingsReport'
+ - 'FileBasedFindingsReport'
+ # - 'LiteFindingsReport'
+
+output-reports:
+ active: true
+ exclude:
+ # - 'TxtOutputReport'
+ # - 'XmlOutputReport'
+ # - 'HtmlOutputReport'
+ # - 'MdOutputReport'
+ # - 'SarifOutputReport'
+
+comments:
+ active: true
+ AbsentOrWrongFileLicense:
+ active: false
+ licenseTemplateFile: 'license.template'
+ licenseTemplateIsRegex: false
+ CommentOverPrivateFunction:
+ active: false
+ CommentOverPrivateProperty:
+ active: false
+ DeprecatedBlockTag:
+ active: false
+ EndOfSentenceFormat:
+ active: false
+ endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
+ KDocReferencesNonPublicProperty:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ OutdatedDocumentation:
+ active: false
+ matchTypeParameters: true
+ matchDeclarationsOrder: true
+ allowParamOnConstructorProperties: false
+ UndocumentedPublicClass:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ searchInNestedClass: true
+ searchInInnerClass: true
+ searchInInnerObject: true
+ searchInInnerInterface: true
+ searchInProtectedClass: false
+ ignoreDefaultCompanionObject: false
+ UndocumentedPublicFunction:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ searchProtectedFunction: false
+ UndocumentedPublicProperty:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ searchProtectedProperty: false
+
+complexity:
+ active: true
+ CognitiveComplexMethod:
+ active: false
+ threshold: 15
+ ComplexCondition:
+ active: true
+ threshold: 4
+ ComplexInterface:
+ active: false
+ threshold: 10
+ includeStaticDeclarations: false
+ includePrivateDeclarations: false
+ ignoreOverloaded: false
+ CyclomaticComplexMethod:
+ active: true
+ threshold: 15
+ ignoreSingleWhenExpression: false
+ ignoreSimpleWhenEntries: false
+ ignoreNestingFunctions: false
+ nestingFunctions:
+ - 'also'
+ - 'apply'
+ - 'forEach'
+ - 'isNotNull'
+ - 'ifNull'
+ - 'let'
+ - 'run'
+ - 'use'
+ - 'with'
+ LabeledExpression:
+ active: false
+ ignoredLabels: []
+ LargeClass:
+ active: true
+ threshold: 600
+ LongMethod:
+ active: true
+ threshold: 60
+ LongParameterList:
+ active: true
+ functionThreshold: 6
+ constructorThreshold: 7
+ ignoreDefaultParameters: false
+ ignoreDataClasses: true
+ ignoreAnnotatedParameter: []
+ MethodOverloading:
+ active: false
+ threshold: 6
+ NamedArguments:
+ active: false
+ threshold: 3
+ ignoreArgumentsMatchingNames: false
+ NestedBlockDepth:
+ active: true
+ threshold: 4
+ NestedScopeFunctions:
+ active: false
+ threshold: 1
+ functions:
+ - 'kotlin.apply'
+ - 'kotlin.run'
+ - 'kotlin.with'
+ - 'kotlin.let'
+ - 'kotlin.also'
+ ReplaceSafeCallChainWithRun:
+ active: false
+ StringLiteralDuplication:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ threshold: 3
+ ignoreAnnotation: true
+ excludeStringsWithLessThan5Characters: true
+ ignoreStringsRegex: '$^'
+ TooManyFunctions:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ thresholdInFiles: 11
+ thresholdInClasses: 11
+ thresholdInInterfaces: 11
+ thresholdInObjects: 11
+ thresholdInEnums: 11
+ ignoreDeprecated: false
+ ignorePrivate: false
+ ignoreOverridden: false
+ ignoreAnnotatedFunctions: []
+
+coroutines:
+ active: true
+ GlobalCoroutineUsage:
+ active: false
+ InjectDispatcher:
+ active: true
+ dispatcherNames:
+ - 'IO'
+ - 'Default'
+ - 'Unconfined'
+ RedundantSuspendModifier:
+ active: true
+ SleepInsteadOfDelay:
+ active: true
+ SuspendFunSwallowedCancellation:
+ active: false
+ SuspendFunWithCoroutineScopeReceiver:
+ active: false
+ SuspendFunWithFlowReturnType:
+ active: true
+
+empty-blocks:
+ active: true
+ EmptyCatchBlock:
+ active: true
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ EmptyClassBlock:
+ active: true
+ EmptyDefaultConstructor:
+ active: true
+ EmptyDoWhileBlock:
+ active: true
+ EmptyElseBlock:
+ active: true
+ EmptyFinallyBlock:
+ active: true
+ EmptyForBlock:
+ active: true
+ EmptyFunctionBlock:
+ active: true
+ ignoreOverridden: false
+ EmptyIfBlock:
+ active: true
+ EmptyInitBlock:
+ active: true
+ EmptyKtFile:
+ active: true
+ EmptySecondaryConstructor:
+ active: true
+ EmptyTryBlock:
+ active: true
+ EmptyWhenBlock:
+ active: true
+ EmptyWhileBlock:
+ active: true
+
+exceptions:
+ active: true
+ ExceptionRaisedInUnexpectedLocation:
+ active: true
+ methodNames:
+ - 'equals'
+ - 'finalize'
+ - 'hashCode'
+ - 'toString'
+ InstanceOfCheckForException:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ NotImplementedDeclaration:
+ active: false
+ ObjectExtendsThrowable:
+ active: false
+ PrintStackTrace:
+ active: true
+ RethrowCaughtException:
+ active: true
+ ReturnFromFinally:
+ active: true
+ ignoreLabeled: false
+ SwallowedException:
+ active: true
+ ignoredExceptionTypes:
+ - 'InterruptedException'
+ - 'MalformedURLException'
+ - 'NumberFormatException'
+ - 'ParseException'
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ ThrowingExceptionFromFinally:
+ active: true
+ ThrowingExceptionInMain:
+ active: false
+ ThrowingExceptionsWithoutMessageOrCause:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ exceptions:
+ - 'ArrayIndexOutOfBoundsException'
+ - 'Exception'
+ - 'IllegalArgumentException'
+ - 'IllegalMonitorStateException'
+ - 'IllegalStateException'
+ - 'IndexOutOfBoundsException'
+ - 'NullPointerException'
+ - 'RuntimeException'
+ - 'Throwable'
+ ThrowingNewInstanceOfSameException:
+ active: true
+ TooGenericExceptionCaught:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ exceptionNames:
+ - 'ArrayIndexOutOfBoundsException'
+ - 'Error'
+ - 'Exception'
+ - 'IllegalMonitorStateException'
+ - 'IndexOutOfBoundsException'
+ - 'NullPointerException'
+ - 'RuntimeException'
+ - 'Throwable'
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ TooGenericExceptionThrown:
+ active: true
+ exceptionNames:
+ - 'Error'
+ - 'Exception'
+ - 'RuntimeException'
+ - 'Throwable'
+
+naming:
+ active: true
+ BooleanPropertyNaming:
+ active: false
+ allowedPattern: '^(is|has|are)'
+ ClassNaming:
+ active: true
+ classPattern: '[A-Z][a-zA-Z0-9]*'
+ ConstructorParameterNaming:
+ active: true
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ privateParameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ EnumNaming:
+ active: true
+ enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
+ ForbiddenClassName:
+ active: false
+ forbiddenName: []
+ FunctionMaxLength:
+ active: false
+ maximumFunctionNameLength: 30
+ FunctionMinLength:
+ active: false
+ minimumFunctionNameLength: 3
+ FunctionNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ functionPattern: '[a-z][a-zA-Z0-9]*'
+ excludeClassPattern: '$^'
+ FunctionParameterNaming:
+ active: true
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ InvalidPackageDeclaration:
+ active: true
+ rootPackage: ''
+ requireRootInDeclaration: false
+ LambdaParameterNaming:
+ active: false
+ parameterPattern: '[a-z][A-Za-z0-9]*|_'
+ MatchingDeclarationName:
+ active: true
+ mustBeFirst: true
+ multiplatformTargets:
+ - 'ios'
+ - 'android'
+ - 'js'
+ - 'jvm'
+ - 'native'
+ - 'iosArm64'
+ - 'iosX64'
+ - 'macosX64'
+ - 'mingwX64'
+ - 'linuxX64'
+ MemberNameEqualsClassName:
+ active: true
+ ignoreOverridden: true
+ NoNameShadowing:
+ active: true
+ NonBooleanPropertyPrefixedWithIs:
+ active: false
+ ObjectPropertyNaming:
+ active: true
+ constantPattern: '[A-Za-z][_A-Za-z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
+ PackageNaming:
+ active: true
+ packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
+ TopLevelPropertyNaming:
+ active: true
+ constantPattern: '[A-Z][_A-Z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
+ VariableMaxLength:
+ active: false
+ maximumVariableNameLength: 64
+ VariableMinLength:
+ active: false
+ minimumVariableNameLength: 1
+ VariableNaming:
+ active: true
+ variablePattern: '[a-z][A-Za-z0-9]*'
+ privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+
+performance:
+ active: true
+ ArrayPrimitive:
+ active: true
+ CouldBeSequence:
+ active: false
+ threshold: 3
+ ForEachOnRange:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ SpreadOperator:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ UnnecessaryPartOfBinaryExpression:
+ active: false
+ UnnecessaryTemporaryInstantiation:
+ active: true
+
+potential-bugs:
+ active: true
+ AvoidReferentialEquality:
+ active: true
+ forbiddenTypePatterns:
+ - 'kotlin.String'
+ CastNullableToNonNullableType:
+ active: false
+ CastToNullableType:
+ active: false
+ Deprecation:
+ active: false
+ DontDowncastCollectionTypes:
+ active: false
+ DoubleMutabilityForCollection:
+ active: true
+ mutableTypes:
+ - 'kotlin.collections.MutableList'
+ - 'kotlin.collections.MutableMap'
+ - 'kotlin.collections.MutableSet'
+ - 'java.util.ArrayList'
+ - 'java.util.LinkedHashSet'
+ - 'java.util.HashSet'
+ - 'java.util.LinkedHashMap'
+ - 'java.util.HashMap'
+ ElseCaseInsteadOfExhaustiveWhen:
+ active: false
+ ignoredSubjectTypes: []
+ EqualsAlwaysReturnsTrueOrFalse:
+ active: true
+ EqualsWithHashCodeExist:
+ active: true
+ ExitOutsideMain:
+ active: false
+ ExplicitGarbageCollectionCall:
+ active: true
+ HasPlatformType:
+ active: true
+ IgnoredReturnValue:
+ active: true
+ restrictToConfig: true
+ returnValueAnnotations:
+ - 'CheckResult'
+ - '*.CheckResult'
+ - 'CheckReturnValue'
+ - '*.CheckReturnValue'
+ ignoreReturnValueAnnotations:
+ - 'CanIgnoreReturnValue'
+ - '*.CanIgnoreReturnValue'
+ returnValueTypes:
+ - 'kotlin.sequences.Sequence'
+ - 'kotlinx.coroutines.flow.*Flow'
+ - 'java.util.stream.*Stream'
+ ignoreFunctionCall: []
+ ImplicitDefaultLocale:
+ active: true
+ ImplicitUnitReturnType:
+ active: false
+ allowExplicitReturnType: true
+ InvalidRange:
+ active: true
+ IteratorHasNextCallsNextMethod:
+ active: true
+ IteratorNotThrowingNoSuchElementException:
+ active: true
+ LateinitUsage:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ ignoreOnClassesPattern: ''
+ MapGetWithNotNullAssertionOperator:
+ active: true
+ MissingPackageDeclaration:
+ active: false
+ excludes: ['**/*.kts']
+ NullCheckOnMutableProperty:
+ active: false
+ NullableToStringCall:
+ active: false
+ PropertyUsedBeforeDeclaration:
+ active: false
+ UnconditionalJumpStatementInLoop:
+ active: false
+ UnnecessaryNotNullCheck:
+ active: false
+ UnnecessaryNotNullOperator:
+ active: true
+ UnnecessarySafeCall:
+ active: true
+ UnreachableCatchBlock:
+ active: true
+ UnreachableCode:
+ active: true
+ UnsafeCallOnNullableType:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ UnsafeCast:
+ active: true
+ UnusedUnaryOperator:
+ active: true
+ UselessPostfixExpression:
+ active: true
+ WrongEqualsTypeParameter:
+ active: true
+
+style:
+ active: true
+ AlsoCouldBeApply:
+ active: false
+ BracesOnIfStatements:
+ active: false
+ singleLine: 'never'
+ multiLine: 'always'
+ BracesOnWhenStatements:
+ active: false
+ singleLine: 'necessary'
+ multiLine: 'consistent'
+ CanBeNonNullable:
+ active: false
+ CascadingCallWrapping:
+ active: false
+ includeElvis: true
+ ClassOrdering:
+ active: false
+ CollapsibleIfStatements:
+ active: false
+ DataClassContainsFunctions:
+ active: false
+ conversionFunctionPrefix:
+ - 'to'
+ allowOperators: false
+ DataClassShouldBeImmutable:
+ active: false
+ DestructuringDeclarationWithTooManyEntries:
+ active: true
+ maxDestructuringEntries: 3
+ DoubleNegativeLambda:
+ active: false
+ negativeFunctions:
+ - reason: 'Use `takeIf` instead.'
+ value: 'takeUnless'
+ - reason: 'Use `all` instead.'
+ value: 'none'
+ negativeFunctionNameParts:
+ - 'not'
+ - 'non'
+ EqualsNullCall:
+ active: true
+ EqualsOnSignatureLine:
+ active: false
+ ExplicitCollectionElementAccessMethod:
+ active: false
+ ExplicitItLambdaParameter:
+ active: true
+ ExpressionBodySyntax:
+ active: false
+ includeLineWrapping: false
+ ForbiddenAnnotation:
+ active: false
+ annotations:
+ - reason: 'it is a java annotation. Use `Suppress` instead.'
+ value: 'java.lang.SuppressWarnings'
+ - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.'
+ value: 'java.lang.Deprecated'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.'
+ value: 'java.lang.annotation.Documented'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.'
+ value: 'java.lang.annotation.Target'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.'
+ value: 'java.lang.annotation.Retention'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.'
+ value: 'java.lang.annotation.Repeatable'
+ - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265'
+ value: 'java.lang.annotation.Inherited'
+ ForbiddenComment:
+ active: true
+ comments:
+ - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
+ value: 'FIXME:'
+ - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
+ value: 'STOPSHIP:'
+ - reason: 'Forbidden TODO todo marker in comment, please do the changes.'
+ value: 'TODO:'
+ allowedPatterns: ''
+ ForbiddenImport:
+ active: false
+ imports: []
+ forbiddenPatterns: ''
+ ForbiddenMethodCall:
+ active: false
+ methods:
+ - reason: 'print does not allow you to configure the output stream. Use a logger instead.'
+ value: 'kotlin.io.print'
+ - reason: 'println does not allow you to configure the output stream. Use a logger instead.'
+ value: 'kotlin.io.println'
+ ForbiddenSuppress:
+ active: false
+ rules: []
+ ForbiddenVoid:
+ active: true
+ ignoreOverridden: false
+ ignoreUsageInGenerics: false
+ FunctionOnlyReturningConstant:
+ active: true
+ ignoreOverridableFunction: true
+ ignoreActualFunction: true
+ excludedFunctions: []
+ LoopWithTooManyJumpStatements:
+ active: true
+ maxJumpCount: 1
+ MagicNumber:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts']
+ ignoreNumbers:
+ - '-1'
+ - '0'
+ - '1'
+ - '2'
+ ignoreHashCodeFunction: true
+ ignorePropertyDeclaration: false
+ ignoreLocalVariableDeclaration: false
+ ignoreConstantDeclaration: true
+ ignoreCompanionObjectPropertyDeclaration: true
+ ignoreAnnotation: false
+ ignoreNamedArgument: true
+ ignoreEnums: false
+ ignoreRanges: false
+ ignoreExtensionFunctions: true
+ MandatoryBracesLoops:
+ active: false
+ MaxChainedCallsOnSameLine:
+ active: false
+ maxChainedCalls: 5
+ MaxLineLength:
+ active: true
+ maxLineLength: 120
+ excludePackageStatements: true
+ excludeImportStatements: true
+ excludeCommentStatements: false
+ excludeRawStrings: true
+ MayBeConst:
+ active: true
+ ModifierOrder:
+ active: true
+ MultilineLambdaItParameter:
+ active: false
+ MultilineRawStringIndentation:
+ active: false
+ indentSize: 4
+ trimmingMethods:
+ - 'trimIndent'
+ - 'trimMargin'
+ NestedClassesVisibility:
+ active: true
+ NewLineAtEndOfFile:
+ active: true
+ NoTabs:
+ active: false
+ NullableBooleanCheck:
+ active: false
+ ObjectLiteralToLambda:
+ active: true
+ OptionalAbstractKeyword:
+ active: true
+ OptionalUnit:
+ active: false
+ PreferToOverPairSyntax:
+ active: false
+ ProtectedMemberInFinalClass:
+ active: true
+ RedundantExplicitType:
+ active: false
+ RedundantHigherOrderMapUsage:
+ active: true
+ RedundantVisibilityModifierRule:
+ active: false
+ ReturnCount:
+ active: true
+ max: 2
+ excludedFunctions:
+ - 'equals'
+ excludeLabeled: false
+ excludeReturnFromLambda: true
+ excludeGuardClauses: false
+ SafeCast:
+ active: true
+ SerialVersionUIDInSerializableClass:
+ active: true
+ SpacingBetweenPackageAndImports:
+ active: false
+ StringShouldBeRawString:
+ active: false
+ maxEscapedCharacterCount: 2
+ ignoredCharacters: []
+ ThrowsCount:
+ active: true
+ max: 2
+ excludeGuardClauses: false
+ TrailingWhitespace:
+ active: false
+ TrimMultilineRawString:
+ active: false
+ trimmingMethods:
+ - 'trimIndent'
+ - 'trimMargin'
+ UnderscoresInNumericLiterals:
+ active: false
+ acceptableLength: 4
+ allowNonStandardGrouping: false
+ UnnecessaryAbstractClass:
+ active: true
+ UnnecessaryAnnotationUseSiteTarget:
+ active: false
+ UnnecessaryApply:
+ active: true
+ UnnecessaryBackticks:
+ active: false
+ UnnecessaryBracesAroundTrailingLambda:
+ active: false
+ UnnecessaryFilter:
+ active: true
+ UnnecessaryInheritance:
+ active: true
+ UnnecessaryInnerClass:
+ active: false
+ UnnecessaryLet:
+ active: false
+ UnnecessaryParentheses:
+ active: false
+ allowForUnclearPrecedence: false
+ UntilInsteadOfRangeTo:
+ active: false
+ UnusedImports:
+ active: false
+ UnusedParameter:
+ active: true
+ allowedNames: 'ignored|expected'
+ UnusedPrivateClass:
+ active: true
+ UnusedPrivateMember:
+ active: true
+ allowedNames: ''
+ UnusedPrivateProperty:
+ active: true
+ allowedNames: '_|ignored|expected|serialVersionUID'
+ UseAnyOrNoneInsteadOfFind:
+ active: true
+ UseArrayLiteralsInAnnotations:
+ active: true
+ UseCheckNotNull:
+ active: true
+ UseCheckOrError:
+ active: true
+ UseDataClass:
+ active: false
+ allowVars: false
+ UseEmptyCounterpart:
+ active: false
+ UseIfEmptyOrIfBlank:
+ active: false
+ UseIfInsteadOfWhen:
+ active: false
+ ignoreWhenContainingVariableDeclaration: false
+ UseIsNullOrEmpty:
+ active: true
+ UseLet:
+ active: false
+ UseOrEmpty:
+ active: true
+ UseRequire:
+ active: true
+ UseRequireNotNull:
+ active: true
+ UseSumOfInsteadOfFlatMapSize:
+ active: false
+ UselessCallOnNotNull:
+ active: true
+ UtilityClassWithPublicConstructor:
+ active: true
+ VarCouldBeVal:
+ active: true
+ ignoreLateinitVar: false
+ WildcardImport:
+ active: true
+ excludeImports:
+ - 'java.util.*'
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index ae614598..51d46658 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -22,6 +22,13 @@ platform :android do
# )
# end
+ # Lane for detekt
+ desc "Run Detekt for code analysis"
+ lane :detekt do
+ gradle(task: "detekt")
+ end
+
+
desc "Runs all the tests"
lane :test do
gradle(task: "test")
diff --git a/fastlane/README.md b/fastlane/README.md
index 855be4cc..4f77c842 100644
--- a/fastlane/README.md
+++ b/fastlane/README.md
@@ -15,6 +15,14 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
## Android
+### android detekt
+
+```sh
+[bundle exec] fastlane android detekt
+```
+
+Run Detekt for code analysis
+
### android test
```sh
diff --git a/fastlane/report.xml b/fastlane/report.xml
index 9b2b379c..32225927 100644
--- a/fastlane/report.xml
+++ b/fastlane/report.xml
@@ -5,19 +5,12 @@
-
+
-
-
-
-
-
-
-
-
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 00000000..ffaeef1e
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,13 @@
+[versions]
+detekt="1.23.8"
+detekt-compose="0.4.22"
+
+[libraries]
+detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" }
+detekt-rules-libraries = { group = "io.gitlab.arturbosch.detekt", name = "detekt-rules-libraries", version.ref = "detekt" }
+detekt-rules-authors = { group = "io.gitlab.arturbosch.detekt", name = "detekt-rules-ruleauthors", version.ref = "detekt" }
+detekt-compose = { group = "io.nlopez.compose.rules", name = "detekt", version.ref = "detekt-compose" }
+
+[plugins]
+
+detekt-plugin = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 91148948..5a016c74 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip