From 0cc4102f31d10f81019ebc309d359aa9f00cdd43 Mon Sep 17 00:00:00 2001 From: Elelan's Macbook Pro Date: Sun, 23 Feb 2025 17:05:27 +0530 Subject: [PATCH 01/14] refactor & rewrite wip --- app/build.gradle.kts | 59 +- app/src/main/AndroidManifest.xml | 58 +- .../opendasharchive/openarchive/SaveApp.kt | 45 +- .../openarchive/core/di/CoreModule.kt | 18 + .../openarchive/core/di/FeaturesModule.kt | 32 - .../openarchive/core/di/PasscodeModule.kt | 48 + .../presentation/components/PrimaryButton.kt | 48 +- .../core/presentation/theme/Colors.kt | 41 +- .../core/presentation/theme/Preview.kt | 20 + .../openarchive/db/MediaViewHolder.kt | 49 -- .../opendasharchive/openarchive/db/Space.kt | 108 ++- .../extensions/BottomSheetExtensions.kt | 57 ++ .../openarchive/features/core/Accordion.kt | 171 ++++ .../features/core/BaseComposeActivity.kt | 56 ++ .../openarchive/features/core/BaseFragment.kt | 7 + .../features/core/dialog/BaseDialog.kt | 33 +- .../core/dialog/DialogConfigBuilder.kt | 45 +- .../features/folders/AddFolderActivity.kt | 42 +- .../features/folders/AddFolderScreen.kt | 163 ++++ .../features/folders/BrowseFolderScreen.kt | 103 +++ .../features/folders/BrowseFoldersActivity.kt | 105 --- .../features/folders/BrowseFoldersAdapter.kt | 8 +- .../features/folders/BrowseFoldersFragment.kt | 109 +++ .../folders/BrowseFoldersViewModel.kt | 6 +- .../folders/CreateNewFolderActivity.kt | 127 --- .../folders/CreateNewFolderFragment.kt | 156 ++++ .../presentation/InternetArchiveFragment.kt | 20 +- .../presentation/components/BundleExt.kt | 4 +- .../details/InternetArchiveDetailsScreen.kt | 44 +- .../login/InternetArchiveLoginScreen.kt | 17 +- .../openarchive/features/main/HomeActivity.kt | 220 +++++ .../openarchive/features/main/MainActivity.kt | 825 ++++++++++++------ .../features/main/MainMediaFragment.kt | 78 +- .../features/main/MainViewModel.kt | 38 + .../features/main/SectionViewHolder.kt | 29 +- .../main/adapters/FolderDrawerAdapter.kt | 104 +++ .../main/adapters/MainMediaAdapter.kt | 347 ++++++++ .../main/adapters/MainMediaViewHolder.kt | 209 +++++ .../main/adapters/SpaceDrawerAdapter.kt | 146 ++++ .../features/main/ui/HomeScreen.kt | 382 ++++++++ .../features/main/ui/MainMediaScreen.kt | 414 +++++++++ .../features/main/ui/MainMediaViewModel.kt | 8 + .../features/main/ui/MediaCacheScreen.kt | 178 ++++ .../main/ui/components/ExpandableSpaceList.kt | 171 ++++ .../main/ui/components/FolderOptionsPopup.kt | 59 ++ .../features/main/ui/components/HomeAppBar.kt | 79 ++ .../main/ui/components/MainBottomBar.kt | 97 ++ .../main/ui/components/MainDrawerContent.kt | 219 +++++ .../features/media/PreviewAdapter.kt | 13 +- .../features/media/ReviewActivity.kt | 11 +- .../media/adapter/PreviewViewHolder.kt | 171 ++++ .../features/onboarding/SpaceSetupActivity.kt | 322 ++----- .../features/settings/CcSelector.kt | 119 --- .../settings/CreativeCommonsLicenseManager.kt | 119 +++ .../features/settings/EditFolderActivity.kt | 6 +- .../features/settings/ProofModeScreen.kt | 199 +++++ .../settings/ProofModeSettingsActivity.kt | 102 ++- .../features/settings/SettingsFragment.kt | 51 +- .../features/settings/SettingsScreen.kt | 17 +- .../features/settings/SpaceSetupFragment.kt | 69 +- .../settings/SpaceSetupSuccessFragment.kt | 13 +- .../features/settings/passcode/AppConfig.kt | 3 +- .../passcode/components/DefaultScaffold.kt | 4 + .../passcode/components/NumericKeypad.kt | 63 +- .../passcode_entry/PasscodeEntryScreen.kt | 105 +-- .../passcode_entry/PasscodeEntryViewModel.kt | 6 + .../passcode_setup/PasscodeSetupActivity.kt | 12 +- .../passcode_setup/PasscodeSetupScreen.kt | 103 +-- .../passcode_setup/PasscodeSetupViewModel.kt | 22 +- .../features/spaces/ServerOptionItem.kt | 123 +++ .../features/spaces/SpaceListFragment.kt | 85 ++ .../features/spaces/SpaceListScreen.kt | 115 +++ .../features/spaces/SpaceSetupScreen.kt | 103 +++ .../features/spaces/SpacesActivity.kt | 80 -- .../services/gdrive/GDriveFragment.kt | 15 +- .../snowbird/SnowbirdCreateGroupFragment.kt | 24 +- .../snowbird/SnowbirdFileListFragment.kt | 5 +- .../services/snowbird/SnowbirdFragment.kt | 75 +- .../snowbird/SnowbirdGroupListFragment.kt | 59 +- .../snowbird/SnowbirdJoinGroupFragment.kt | 2 +- .../snowbird/SnowbirdRepoListFragment.kt | 28 +- .../snowbird/SnowbirdShareFragment.kt | 4 +- .../services/webdav/WebDavFragment.kt | 184 +++- .../webdav/WebDavSetupLicenseFragment.kt | 24 +- .../upload/UploadManagerFragment.kt | 10 +- app/src/main/res/anim/popdown_anim.xml | 8 + app/src/main/res/anim/popup_anim.xml | 8 + app/src/main/res/drawable/bg_pill_white.xml | 16 + app/src/main/res/drawable/button.xml | 23 +- app/src/main/res/drawable/ic_arrow_submit.xml | 15 + .../drawable/ic_browse_existing_folders.xml | 11 + app/src/main/res/drawable/ic_close.xml | 10 + .../res/drawable/ic_create_new_folder.xml | 11 + app/src/main/res/drawable/ic_done.xml | 9 + app/src/main/res/drawable/ic_dweb.xml | 10 + app/src/main/res/drawable/ic_edit_folder.xml | 26 + app/src/main/res/drawable/ic_folder_new.xml | 9 + app/src/main/res/drawable/ic_info_outline.xml | 5 + app/src/main/res/drawable/ic_pdf.xml | 33 + app/src/main/res/drawable/ic_space_dweb.xml | 11 + .../drawable/ic_space_interent_archive.xml | 10 + .../res/drawable/ic_space_private_server.xml | 13 + app/src/main/res/drawable/ic_trash.xml | 13 + .../list_item_background_selector.xml | 18 + app/src/main/res/drawable/welcome_arrow.xml | 2 +- .../res/layout/activity_create_new_folder.xml | 64 -- app/src/main/res/layout/activity_main.xml | 413 +++------ app/src/main/res/layout/activity_review.xml | 13 +- .../layout/activity_settings_container.xml | 53 ++ .../main/res/layout/activity_space_setup.xml | 33 +- app/src/main/res/layout/activity_spaces.xml | 43 - app/src/main/res/layout/activity_webdav.xml | 11 + app/src/main/res/layout/content_cc.xml | 24 +- app/src/main/res/layout/content_main.xml | 284 ++++++ app/src/main/res/layout/folder_row.xml | 9 +- ...olders.xml => fragment_browse_folders.xml} | 6 +- .../res/layout/fragment_create_new_folder.xml | 131 +++ .../main/res/layout/fragment_main_media.xml | 37 +- ...s.xml => fragment_snowbird_group_list.xml} | 0 .../main/res/layout/fragment_space_list.xml | 16 + .../main/res/layout/fragment_space_setup.xml | 579 ++++++------ .../layout/fragment_space_setup_success.xml | 5 +- app/src/main/res/layout/fragment_web_dav.xml | 145 +-- .../layout/fragment_webdav_setup_license.xml | 18 +- .../main/res/layout/popup_folder_options.xml | 58 ++ app/src/main/res/layout/rv_drawer_row.xml | 30 + app/src/main/res/menu/menu_confirm.xml | 9 + app/src/main/res/menu/menu_main.xml | 12 +- .../res/menu/menu_main_edit_folder_bar.xml | 14 + .../res/navigation/space_setup_navigation.xml | 296 +++++++ app/src/main/res/values-night/colors.xml | 2 +- app/src/main/res/values/colors.xml | 5 +- app/src/main/res/values/dimens.xml | 5 +- app/src/main/res/values/strings.xml | 43 +- app/src/main/res/values/styles.xml | 49 +- app/src/main/res/xml/prefs_general.xml | 18 +- app/src/main/res/xml/prefs_proof_mode.xml | 26 +- .../openarchive/MainMediaAdapterTest.kt | 186 ++++ build.gradle.kts | 10 +- 139 files changed, 8294 insertions(+), 2482 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/di/PasscodeModule.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/extensions/BottomSheetExtensions.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/core/Accordion.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/core/BaseComposeActivity.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderScreen.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFolderScreen.kt delete mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersActivity.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersFragment.kt delete mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderActivity.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderFragment.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/HomeActivity.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/MainViewModel.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/FolderDrawerAdapter.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/MainMediaAdapter.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/MainMediaViewHolder.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/SpaceDrawerAdapter.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/ui/HomeScreen.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaScreen.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaViewModel.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MediaCacheScreen.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/ExpandableSpaceList.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/FolderOptionsPopup.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/HomeAppBar.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/MainBottomBar.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/main/ui/components/MainDrawerContent.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/media/adapter/PreviewViewHolder.kt delete mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/settings/CcSelector.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/settings/CreativeCommonsLicenseManager.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeScreen.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/spaces/ServerOptionItem.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListFragment.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListScreen.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceSetupScreen.kt delete mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpacesActivity.kt create mode 100644 app/src/main/res/anim/popdown_anim.xml create mode 100644 app/src/main/res/anim/popup_anim.xml create mode 100644 app/src/main/res/drawable/bg_pill_white.xml create mode 100644 app/src/main/res/drawable/ic_arrow_submit.xml create mode 100644 app/src/main/res/drawable/ic_browse_existing_folders.xml create mode 100644 app/src/main/res/drawable/ic_close.xml create mode 100644 app/src/main/res/drawable/ic_create_new_folder.xml create mode 100644 app/src/main/res/drawable/ic_done.xml create mode 100644 app/src/main/res/drawable/ic_dweb.xml create mode 100644 app/src/main/res/drawable/ic_edit_folder.xml create mode 100644 app/src/main/res/drawable/ic_folder_new.xml create mode 100644 app/src/main/res/drawable/ic_info_outline.xml create mode 100644 app/src/main/res/drawable/ic_pdf.xml create mode 100644 app/src/main/res/drawable/ic_space_dweb.xml create mode 100644 app/src/main/res/drawable/ic_space_interent_archive.xml create mode 100644 app/src/main/res/drawable/ic_space_private_server.xml create mode 100644 app/src/main/res/drawable/ic_trash.xml create mode 100644 app/src/main/res/drawable/list_item_background_selector.xml delete mode 100644 app/src/main/res/layout/activity_create_new_folder.xml delete mode 100644 app/src/main/res/layout/activity_spaces.xml create mode 100644 app/src/main/res/layout/content_main.xml rename app/src/main/res/layout/{activity_browse_folders.xml => fragment_browse_folders.xml} (92%) create mode 100644 app/src/main/res/layout/fragment_create_new_folder.xml rename app/src/main/res/layout/{fragment_snowbird_list_groups.xml => fragment_snowbird_group_list.xml} (100%) create mode 100644 app/src/main/res/layout/fragment_space_list.xml create mode 100644 app/src/main/res/layout/popup_folder_options.xml create mode 100644 app/src/main/res/layout/rv_drawer_row.xml create mode 100644 app/src/main/res/menu/menu_confirm.xml create mode 100644 app/src/main/res/menu/menu_main_edit_folder_bar.xml create mode 100644 app/src/main/res/navigation/space_setup_navigation.xml create mode 100644 app/src/test/java/net/opendasharchive/openarchive/MainMediaAdapterTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 823bbcdc..c71e2075 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,6 +6,7 @@ plugins { id("org.jetbrains.kotlin.plugin.compose") id("org.jetbrains.kotlin.plugin.serialization") id("com.google.devtools.ksp") + id("androidx.navigation.safeargs.kotlin") } android { @@ -88,6 +89,14 @@ android { abortOnError = false } + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + + namespace = "net.opendasharchive.openarchive" configurations.all { @@ -100,9 +109,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.7" + 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") @@ -112,23 +127,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.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") @@ -136,9 +155,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") @@ -147,24 +167,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 @@ -247,7 +278,7 @@ 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") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2459bf39..160cc45b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -100,11 +100,43 @@ + android:name=".features.main.HomeActivity" + android:exported="true" + android:theme="@style/SaveAppTheme.NoActionBar" + android:screenOrientation="portrait"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + () + ) + } } 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..1282bc25 --- /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 = false + ) + } + + 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..a3af9a36 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 @@ -47,18 +47,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,8 +70,8 @@ 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, @@ -92,18 +92,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/db/MediaViewHolder.kt b/app/src/main/java/net/opendasharchive/openarchive/db/MediaViewHolder.kt index 036dea04..646f57d0 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/db/MediaViewHolder.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/db/MediaViewHolder.kt @@ -83,55 +83,6 @@ abstract class MediaViewHolder(protected val binding: ViewBinding) : 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) ) { 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/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/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..69222219 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 @@ -57,7 +57,7 @@ fun BaseDialog( positiveButton: ButtonData, 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() @@ -165,7 +169,7 @@ fun BaseDialog( } - } + } } @@ -245,6 +249,7 @@ fun DialogHost(dialogStateManager: DialogStateManager) { } @Preview +@Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun BaseDialogPreview() { DefaultBoxPreview { @@ -262,6 +267,7 @@ private fun BaseDialogPreview() { } @Preview +@Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun WarningDialogPreview() { DefaultBoxPreview { @@ -282,6 +288,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..a74a2bbd 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 @@ -19,6 +19,7 @@ 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 +35,17 @@ data class DialogConfig( val type: DialogType, val title: UiText, val message: UiText, - val icon: UiImage?, - val iconColor: Color?, + val icon: UiImage? = null, + val iconColor: Color? = null, val positiveButton: ButtonData, 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, ) // -------------------------------------------------------------------- @@ -93,6 +95,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)) @@ -167,7 +172,8 @@ class DialogBuilder { checkboxText = checkboxText, onCheckboxChanged = onCheckboxChanged, backgroundColor = finalBackgroundColor, - cornerRadius = finalCornerRadius + cornerRadius = finalCornerRadius, + onDismissAction = _onDismissAction ) } @@ -236,7 +242,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) } @@ -332,6 +338,33 @@ 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 + } + } +} + /** * 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..fc348282 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderScreen.kt @@ -0,0 +1,163 @@ +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(vertical = 24.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(16.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 = 24.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(24.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(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..372b7038 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersFragment.kt @@ -0,0 +1,109 @@ +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.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() + + requireActivity().setResult(RESULT_OK, Intent().apply { + putExtra(AddFolderActivity.EXTRA_FOLDER_ID, project.id) + }) + 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..a58849c6 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderFragment.kt @@ -0,0 +1,156 @@ +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() + } + + if (Space.current?.license != null) { + binding.cc.root.hide() + } else { + CreativeCommonsLicenseManager.initialize(binding.cc) + } + + 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/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/details/InternetArchiveDetailsScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt index b05abbbc..0ee6af9b 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,9 +1,12 @@ 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 @@ -27,6 +30,7 @@ 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 @@ -79,7 +83,7 @@ private fun InternetArchiveDetailsContent( Column { - InternetArchiveHeader() + //InternetArchiveHeader() Spacer(Modifier.height(ThemeDimensions.spacing.large)) @@ -108,22 +112,32 @@ 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 + }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Text( + stringResource(id = R.string.remove_from_app), + fontSize = 18.sp + ) + } + } + + } + + } if (isRemoving) { 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..f396dae8 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 @@ -28,6 +28,7 @@ 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 @@ -276,7 +277,7 @@ fun CustomTextField( imeAction: ImeAction = ImeAction.Next, ) { - TextField( + OutlinedTextField( modifier = modifier.fillMaxWidth(), value = value, enabled = !isLoading, @@ -302,8 +303,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,7 +328,7 @@ fun CustomSecureField( mutableStateOf(false) } - TextField( + OutlinedTextField( modifier = modifier.fillMaxWidth(), value = value, enabled = !isLoading, @@ -350,8 +353,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 b8befd5f..60441a6a 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,34 +1,53 @@ 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.view.Gravity +import android.view.LayoutInflater import android.view.Menu import android.view.MenuItem import android.view.View -import android.widget.Toast +import android.widget.LinearLayout +import android.widget.PopupWindow import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.drawerlayout.widget.DrawerLayout 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.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.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.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 @@ -37,27 +56,30 @@ 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.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 @@ -65,19 +87,33 @@ class MainActivity : BaseActivity() { 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) { @@ -85,16 +121,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() @@ -108,248 +134,475 @@ 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() + + + 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() + 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() + 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() } - - override fun onPageScrollStateChanged(state: Int) {} }) + } - setupBottomNavBar() - + private fun setupNavigationDrawer() { + // Drawer listener resets state on close + binding.root.addDrawerListener(object : DrawerLayout.DrawerListener { + override fun onDrawerClosed(drawerView: View) { + collapseSpacesList() + } - binding.breadcrumbSpace.setOnClickListener { - startActivity(Intent(this, SpacesActivity::class.java)) - } + override fun onDrawerOpened(drawerView: View) { + // + } - 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) - } + override fun onDrawerSlide(drawerView: View, slideOffset: Float) { + // + } + override fun onDrawerStateChanged(newState: Int) { + // + } + }) - if (appConfig.snowbirdEnabled) { + binding.navigationDrawerHeader.setOnClickListener { toggleSpacesList() } + binding.dimOverlay.setOnClickListener { collapseSpacesList() } - checkNotificationPermissions() + mSpaceAdapter = SpaceDrawerAdapter(this) + binding.spaces.layoutManager = LinearLayoutManager(this) + binding.spaces.adapter = mSpaceAdapter - SnowbirdBridge.getInstance().initialize() - val intent = Intent(this, SnowbirdService::class.java) - startForegroundService(intent) + mFolderAdapter = FolderDrawerAdapter(this) + binding.folders.layoutManager = LinearLayoutManager(this) + binding.folders.adapter = mFolderAdapter - handleIntent(intent) + binding.btnAddFolder.scaleAndTintDrawable(Position.Start, 0.75) + binding.btnAddFolder.setOnClickListener { + closeDrawer() + navigateToAddFolder() } + + updateCurrentSpaceAtDrawer() } - private fun handleIntent(intent: Intent) { - if (intent.action == Intent.ACTION_VIEW) { - val uri = intent.data - if (uri?.scheme == "save-veilid") { - processUri(uri) + private fun setupBottomNavBar() { + with(binding.contentMain.bottomNavBar) { + onMyMediaClick = { + mCurrentPagerItem = mSelectedMediaPageIndex + } + onAddClick = { addClicked(AddMediaType.GALLERY) } + onSettingsClick = { + mCurrentPagerItem = mPagerAdapter.settingsIndex + } + + 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) } } } } - private fun processUri(uri: Uri) { - val path = uri.path - val queryParams = uri.queryParameterNames.associateWith { uri.getQueryParameter(it) } - AppLogger.d("Path: $path, QueryParams: $queryParams") + 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 == android.view.inputmethod.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() + } + true + } else false + } + + binding.contentMain.btnRemoveSelected.setOnClickListener { + showDeleteConfirmDialog() + } } - private fun setupBottomNavBar() { - binding.bottomNavBar.onMyMediaClick = { - mCurrentPagerItem = mLastMediaItem + // 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() } + } - binding.bottomNavBar.onAddClick = { - addClicked(AddMediaType.GALLERY) + 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.onSettingsClick = { - mCurrentPagerItem = mPagerAdapter.settingsIndex + // Option to toggle selection mode + popupBinding.menuFolderBarSelectMedia.setOnClickListener { + popup.dismiss() + setFolderBarMode(FolderBarMode.SELECTION) + } + // Rename folder + popupBinding.menuFolderBarRenameFolder.setOnClickListener { + popup.dismiss() + setFolderBarMode(FolderBarMode.EDIT) } - 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) - } - supportFragmentManager.setFragmentResultListener( - AddMediaDialogFragment.RESP_FILES, - this - ) { _, _ -> - addClicked(AddMediaType.FILES) - } - } + private fun showDeleteConfirmDialog() { + 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) + } + ) + ) + ) } - private fun updateBottomNavbar(position: Int) { - binding.bottomNavBar.updateSelectedItem(isSettings = position == mPagerAdapter.settingsIndex) - if (position == mPagerAdapter.settingsIndex) { - binding.breadcrumbContainer.hide() - } else { - // Show the breadcrumb container only if there's any server available - if (Space.current != null) { - binding.breadcrumbContainer.show() - } - } + private fun getCurrentMediaFragment(): MainMediaFragment? { + val currentItem = binding.contentMain.pager.currentItem + return supportFragmentManager.findFragmentByTag("f$currentItem") as? MainMediaFragment } - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - override fun onStart() { - super.onStart() - ProofModeHelper.init(this) { - // Check for any queued uploads and restart, only after ProofMode is correctly initialized. - UploadService.startUploadService(this) + // ----- Drawer Helpers ----- + private fun toggleDrawerState() { + if (binding.root.isDrawerOpen(binding.drawerContent)) { + closeDrawer() + } else { + openDrawer() } } - override fun onResume() { - super.onResume() - - refreshSpace() + private fun openDrawer() { + binding.root.openDrawer(binding.drawerContent) + } - mCurrentPagerItem = mLastItem + private fun closeDrawer() { + binding.root.closeDrawer(binding.drawerContent) + } - if (!Prefs.didCompleteOnboarding) { - startActivity(Intent(this, Onboarding23Activity::class.java)) + private fun toggleSpacesList() { + if (serverListCurOffset == serverListOffset) { + expandSpacesList() + } else { + collapseSpacesList() } + } - importSharedMedia(intent) + 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 } + 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 + } - private fun navigateToFolder(folderId: Long) { - val folderIndex = mPagerAdapter.getProjectIndexById(folderId) - if (folderIndex >= 0) { - binding.pager.setCurrentItem(folderIndex, true) - mCurrentPagerItem = folderIndex + private fun updateCurrentSpaceAtDrawer() { + val drawable = Space.current?.getAvatar(applicationContext) + ?.scaled(R.dimen.avatar_size, applicationContext) + binding.spaceIcon.setImageDrawable(drawable) + mSpaceAdapter.notifyDataSetChanged() + } - } else { - Toast.makeText(this, "Folder not found", Toast.LENGTH_SHORT).show() + // ----- 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 + } + + FolderBarMode.SELECTION -> { + binding.contentMain.folderInfoContainer.visibility = View.GONE + binding.contentMain.folderSelectionContainer.visibility = View.VISIBLE + binding.contentMain.folderEditContainer.visibility = View.GONE + } + + 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.text = getSelectedProject()?.description ?: "" + binding.contentMain.etFolderName.requestFocus() + } } } + private fun updateCurrentFolderVisibility() { + val projects = Space.current?.projects ?: emptyList() + if (mCurrentPagerItem == mPagerAdapter.settingsIndex || projects.isEmpty()) { + binding.contentMain.folderBar.hide() + // Reset to default mode + setFolderBarMode(FolderBarMode.INFO) + } 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) { + binding.contentMain.bottomNavBar.updateSelectedItem(isSettings = position == mPagerAdapter.settingsIndex) + updateCurrentFolderVisibility() } 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.spaceName.text = currentSpace.friendlyName + currentSpace.setAvatar(binding.contentMain.spaceIcon) + } else { + binding.spaceNameLayout.visibility = View.INVISIBLE + binding.contentMain.folderBar.visibility = View.INVISIBLE } + mSpaceAdapter.update(Space.getAll().asSequence().toList()) + updateCurrentSpaceAtDrawer() refreshProjects() + 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() - - } else { - this@MainActivity.binding.breadcrumbFolder.cloak() + binding.contentMain.folderInfoContainer.visibility = View.VISIBLE + project.space?.setAvatar(binding.contentMain.spaceIcon) + binding.contentMain.folderName.text = project.description } - + updateCurrentFolderVisibility() refreshCurrentFolderCount() } @@ -357,172 +610,184 @@ 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)) + } - } else { - binding.folderCount.cloak() + private fun navigateToAddFolder() { + val intent = Intent(this, SpaceSetupActivity::class.java) + 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) } - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) + // ----- 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") - when (requestCode) { - 2 -> Picker.pickMedia(this, mediaLaunchers.imagePickerLauncher) + shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> showNotificationPermissionRationale() + else -> requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } } } - - fun getSelectedProject(): Project? { - return mPagerAdapter.getProject(mCurrentPagerItem) + private fun showNotificationPermissionRationale() { + Utility.showMaterialWarning(this, "Accept!") { Timber.d("thing") } } - fun getSelectedSpace(): Space? { - return Space.current + private fun handleIntent(intent: Intent) { + if (intent.action == Intent.ACTION_VIEW) { + intent.data?.takeIf { it.scheme == "save-veilid" }?.let { processUri(it) } + } } - private fun addClicked(mediaType: AddMediaType) { - - // Check if there's any project selected - if (getSelectedProject() != null) { - - if (Prefs.addMediaHint) { - when (mediaType) { - AddMediaType.CAMERA -> Picker.takePhoto( - this@MainActivity, - mediaLaunchers.cameraLauncher - ) + private fun processUri(uri: Uri) { + val path = uri.path + val queryParams = uri.queryParameterNames.associateWith { uri.getQueryParameter(it) } + AppLogger.d("Path: $path, QueryParams: $queryParams") + } - AddMediaType.GALLERY -> Picker.pickMedia( - this, - mediaLaunchers.imagePickerLauncher - ) + // ----- Overrides ----- + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + return super.onCreateOptionsMenu(menu) + } - 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 - } - ) + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_folders -> { + toggleDrawerState() + 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() - } + else -> super.onOptionsItemSelected(item) } } - 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) - } - } + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when (requestCode) { + 2 -> Picker.pickMedia(this, mediaLaunchers.imagePickerLauncher) } } - private fun showNotificationPermissionRationale() { - Utility.showMaterialWarning(this, "Accept!") { - Timber.d("thing") - } + // ----- Adapter Listeners ----- + override fun onProjectSelected(project: Project) { + binding.root.closeDrawer(binding.drawerContent) + mCurrentPagerItem = mPagerAdapter.projects.indexOf(project) } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun getSelectedProject(): Project? { + return mPagerAdapter.getProject(mCurrentPagerItem) + } - menuInflater.inflate(R.menu.menu_main, menu) + override fun onSpaceSelected(space: Space) { + Space.current = space + refreshSpace() + updateCurrentSpaceAtDrawer() + collapseSpacesList() + binding.root.closeDrawer(binding.drawerContent) + } - return super.onCreateOptionsMenu(menu) + override fun onAddNewSpace() { + collapseSpacesList() + closeDrawer() + val intent = Intent(this, SpaceSetupActivity::class.java) + startActivity(intent) + } + + override fun getSelectedSpace(): Space? { + val currentSpace = Space.current + AppLogger.i("current space requested by adapter... = $currentSpace") + return Space.current } - 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) + fun updateAfterDelete(done: Boolean) { + mMenuDelete?.isVisible = !done + if (done) { + refreshCurrentFolderCount() } } + } 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..a534b7d1 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,12 +21,11 @@ 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.main.adapters.MainMediaAdapter import net.opendasharchive.openarchive.upload.BroadcastManager -import net.opendasharchive.openarchive.util.AlertHelper import net.opendasharchive.openarchive.util.extensions.toggle +import org.koin.androidx.viewmodel.ext.android.activityViewModel import kotlin.collections.set class MainMediaFragment : Fragment() { @@ -47,11 +45,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 +83,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 +93,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,13 +112,13 @@ 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) + getString(R.string.tap_to_add) } else { "Tap the button below to add media folder." } @@ -151,7 +135,6 @@ class MainMediaFragment : Fragment() { 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 +192,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 +219,17 @@ 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(), + data = media, + recyclerView = holder.recyclerView, + checkSelecting = { updateSelectionState() }, + ) holder.recyclerView.adapter = mediaAdapter mAdapters[collection.id] = mediaAdapter @@ -251,6 +238,19 @@ class MainMediaFragment : Fragment() { return holder.root } + //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) 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..09065907 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/MainMediaAdapter.kt @@ -0,0 +1,347 @@ +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.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( + activity: Activity?, + data: List, + private val recyclerView: RecyclerView, + private val supportedStatuses: List = listOf( + Media.Status.Local, + Media.Status.Uploading, + Media.Status.Error + ), + private val checkSelecting: () -> Unit, + private val allowMultiProjectSelection: Boolean = false, +) : RecyclerView.Adapter() { + + companion object { + private const val PAYLOAD_SELECTION = "selection" + private const val PAYLOAD_PROGRESS = "progress" + } + + var media: ArrayList = ArrayList(data) + 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] + when (item.sStatus) { + Media.Status.Local -> { + if (supportedStatuses.contains(Media.Status.Local)) { + mActivity.get()?.let { + PreviewActivity.start(it, item.projectId) + } + } + } + Media.Status.Queued, Media.Status.Uploading -> { + if (supportedStatuses.contains(Media.Status.Uploading)) { + mActivity.get()?.startActivity(Intent(mActivity.get(), UploadManagerActivity::class.java)) + } + } + Media.Status.Error -> { + if (supportedStatuses.contains(Media.Status.Error)) { + mActivity.get()?.let { activity -> + AlertHelper.show( + activity, + activity.getString(R.string.upload_unsuccessful_description), + R.string.upload_unsuccessful, + R.drawable.ic_error, + listOf( + AlertHelper.positiveButton(R.string.retry) { _, _ -> + item.apply { + sStatus = Media.Status.Queued + statusMessage = "" + save() + BroadcastManager.postChange(activity, item.collectionId, item.id) + } + UploadService.startUploadService(activity) + }, + AlertHelper.negativeButton(R.string.remove) { _, _ -> + deleteItem(position) + }, + AlertHelper.neutralButton() + ) + ) + } + } + } + else -> { + // Default behavior if needed. + } + } + } + + 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..65f05f74 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/adapters/MainMediaViewHolder.kt @@ -0,0 +1,209 @@ +package net.opendasharchive.openarchive.features.main.adapters + +import android.annotation.SuppressLint +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.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.RvMediaBoxBinding +import net.opendasharchive.openarchive.db.Media +import net.opendasharchive.openarchive.fragments.VideoRequestHandler +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 mPicasso = Picasso.Builder(mContext) +// .addRequestHandler(VideoRequestHandler(mContext)) +// .build() + + private val imageLoader = ImageLoader.Builder(mContext) + .components { + add(VideoFrameDecoder.Factory()) + } + .build() + + + fun bind(media: Media? = null, isInSelectionMode: Boolean = false, doImageFade: Boolean = true) { + AppLogger.i("Binding media item ${media?.id} with status ${media?.sStatus} and progress ${media?.uploadPercentage}") + 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.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.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.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 if (media?.mimeType?.startsWith("application") == true) { + binding.image.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.ic_pdf)) + binding.image.show() + binding.waveform.hide() + binding.videoIndicator.hide() + } else { + binding.image.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.no_thumbnail)) + 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..39095a0c --- /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.colorBackground + 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/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/SpaceSetupActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt index 81e16511..0afbfa64 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,74 @@ 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 +} + 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) + if (startDestination == StartDestination.SPACE_LIST) { + navGraph.setStartDestination(R.id.fragment_space_list) + } else if (startDestination == StartDestination.ADD_FOLDER) { + navGraph.setStartDestination(R.id.fragment_add_folder) + }else { + navGraph.setStartDestination(R.id.fragment_space_setup) } + navController.graph = navGraph + + appBarConfiguration = AppBarConfiguration(emptySet()) + + setupActionBarWithNavController(navController, appBarConfiguration) + } fun updateToolbarFromFragment(fragment: Fragment) { @@ -87,240 +118,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..a358f144 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 @@ -62,7 +62,7 @@ class EditFolderActivity : BaseActivity() { archiveProject() } - CcSelector.init(mBinding.cc, null) { + CreativeCommonsLicenseManager.initialize(mBinding.cc, null) { mProject.licenseUrl = it mProject.save() } @@ -101,10 +101,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/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 000b96fc..5ee45027 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 @@ -7,12 +7,15 @@ 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.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.text.HtmlCompat import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat @@ -20,6 +23,7 @@ import com.permissionx.guolindev.PermissionX import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview import net.opendasharchive.openarchive.databinding.ActivitySettingsContainerBinding import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.util.Hbks @@ -31,28 +35,23 @@ 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) - 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) @@ -65,11 +64,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 } } } @@ -77,20 +82,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) { @@ -99,8 +107,7 @@ class ProofModeSettingsActivity: BaseActivity() { } else { enableProofModeKeyEncryption(pkePreference) } - } - else { + } else { if (Prefs.proofModeEncryptedPassphrase != null) { Prefs.proofModeEncryptedPassphrase = null @@ -112,8 +119,7 @@ class ProofModeSettingsActivity: BaseActivity() { true } - } - else { + } else { pkePreference?.isVisible = false } } @@ -141,6 +147,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 @@ -158,6 +169,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 { @@ -182,13 +211,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, _ -> @@ -198,7 +230,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 @@ -210,4 +246,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..9e2263ee 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 @@ -11,18 +11,27 @@ 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.features.spaces.SpaceListFragment 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 +80,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 +116,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,15 +127,15 @@ 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 } findPreference(Prefs.THEME)?.setOnPreferenceChangeListener { _, newValue -> 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..9e3ef9ac 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 @@ -17,8 +20,12 @@ import me.zhanghai.compose.preference.preferenceCategory 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 +50,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..c83a7584 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 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..91928ff6 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 = false, + 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..55dc44f9 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)) @@ -130,8 +145,21 @@ private fun NumberButton( 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.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 = "" ) @@ -152,13 +180,26 @@ private fun NumberButton( .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..4a4bd3d9 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() { @@ -31,7 +32,16 @@ class PasscodeSetupActivity : BaseActivity() { 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..9816471b 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 @@ -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, @@ -129,14 +131,7 @@ 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)) @@ -155,6 +150,12 @@ private fun PasscodeSetupScreenContent( isEnabled = !state.isProcessing, onNumberClick = { number -> onAction(PasscodeSetupUiAction.OnNumberClick(number)) + }, + onDeleteClick = { + onAction(PasscodeSetupUiAction.OnBackspaceClick) + }, + onSubmitClick = { + onAction(PasscodeSetupUiAction.OnSubmit) } ) @@ -162,43 +163,43 @@ private fun PasscodeSetupScreenContent( 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 - ), - ) - } - - - } +// 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..9abaf9fd --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/ServerOptionItem.kt @@ -0,0 +1,123 @@ +package net.opendasharchive.openarchive.features.spaces + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.BorderStroke +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.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +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.ListItem +import androidx.compose.material3.ListItemDefaults +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.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.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) + ) { + + ListItem( + modifier = Modifier + .height(100.dp), + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.background + ), + leadingContent = { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = colorResource(R.color.colorTertiary), + modifier = Modifier.size(24.dp) + ) + }, + headlineContent = { + Text( + text = title, + fontWeight = FontWeight.Black, + fontSize = 18.sp + ) + }, + supportingContent = { + Text( + text = subtitle, + fontWeight = FontWeight.Normal, + fontSize = 14.sp + ) + }, + trailingContent = { + Box( + modifier = Modifier.fillMaxHeight(), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Default.ArrowForward, + contentDescription = null, + ) + } + } + ) + + + } +} + +@Preview +@Composable +private fun ServerOptionItemPreview() { + DefaultBoxPreview { + + ServerOptionItem( + iconRes = R.drawable.ic_internet_archive, + title = stringResource(R.string.internet_archive), + subtitle = stringResource(R.string.upload_to_the_internet_archive), + 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..4e688426 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListScreen.kt @@ -0,0 +1,115 @@ +package net.opendasharchive.openarchive.features.spaces + +import android.content.res.Configuration +import android.content.res.Configuration.UI_MODE_NIGHT_YES +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.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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +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(16.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 { + Text( + space.friendlyName, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + space.tType.friendlyName, + color = MaterialTheme.colorScheme.onBackground + ) + } + } +} + +@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..c3ae1d5c --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceSetupScreen.kt @@ -0,0 +1,103 @@ +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(24.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.Bold, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = stringResource(R.string.to_get_started_more_hint), + fontSize = 14.sp, + 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..7e997011 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 @@ -4,24 +4,37 @@ 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.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.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 @@ -51,7 +64,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 +84,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,13 +94,13 @@ 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) { @@ -98,15 +112,21 @@ class WebDavFragment : BaseFragment() { mSpace.save() // Save the entity using SugarORM // Hide the keyboard - val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + 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() + 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() + Snackbar.make(binding.root, "Name cannot be empty", Snackbar.LENGTH_SHORT) + .show() } true // Consume the event @@ -115,6 +135,10 @@ class WebDavFragment : BaseFragment() { } } + CreativeCommonsLicenseManager.initialize(binding.cc, mSpace.license) { + mSpace.license = it + mSpace.save() + } } else { // setup views for creating a new space @@ -122,14 +146,27 @@ 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 + if (BuildConfig.DEBUG) { + binding.server.setText("https://nx27277.your-storageshare.de/") + binding.username.setText("Upul") + binding.password.setText("J7wc(ka_4#9!13h&") + } } binding.btAuthenticate.setOnClickListener { attemptLogin() } binding.btCancel.setOnClickListener { - setFragmentResult(RESP_CANCEL, bundleOf()) + if (isJetpackNavigation) { + findNavController().popBackStack() + } else { + setFragmentResult(RESP_CANCEL, bundleOf()) + } } binding.server.setOnFocusChangeListener { _, hasFocus -> @@ -152,6 +189,53 @@ 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 onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_confirm -> { + //todo: save changes here and show success dialog + saveChanges() + true + } + + else -> false + } + } + } + + requireActivity().addMenuProvider( + menuProvider, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) + } + } + + private fun saveChanges() { + + showSuccess() + } + + private fun showSuccess() { + + 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 fixSpaceUrl(url: CharSequence?): Uri? { @@ -193,8 +277,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 +327,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() { @@ -311,18 +401,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), + positiveButton = 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 +457,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 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..31a27bf0 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 @@ -8,11 +8,12 @@ import android.view.View import android.view.ViewGroup 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() { @@ -43,14 +44,23 @@ class WebDavSetupLicenseFragment: BaseFragment() { 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,8 +92,8 @@ 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() @@ -97,7 +107,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 { 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..82af5cac 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerFragment.kt @@ -1,5 +1,6 @@ package net.opendasharchive.openarchive.upload +import android.app.Dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -10,15 +11,18 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment 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 -open class UploadManagerFragment : Fragment() { +open class UploadManagerFragment : BottomSheetDialogFragment() { companion object { + const val TAG = "ModalBottomSheet-UploadManagerFragment" private val STATUSES = listOf(Media.Status.Uploading, Media.Status.Queued, Media.Status.Error) } @@ -28,6 +32,10 @@ open class UploadManagerFragment : Fragment() { private lateinit var mItemTouchHelper: ItemTouchHelper + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return BottomSheetDialog(requireContext(), R.style.SaveAppFullBottomSheetTheme) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, 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/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/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/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/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/activity_create_new_folder.xml b/app/src/main/res/layout/activity_create_new_folder.xml deleted file mode 100644 index dd194353..00000000 --- a/app/src/main/res/layout/activity_create_new_folder.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ 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..da51114c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,334 +1,157 @@ - - - - + + - - - - - - - + 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:layout_width="24dp" + android:layout_height="24dp" + android:src="@drawable/ic_private_server" + tools:src="@drawable/ic_private_server" /> - - - - - - - + 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_review.xml b/app/src/main/res/layout/activity_review.xml index 7f2b0aa2..e9c2eb16 100644 --- a/app/src/main/res/layout/activity_review.xml +++ b/app/src/main/res/layout/activity_review.xml @@ -39,7 +39,7 @@ app:layout_constraintTop_toTopOf="parent" app:waveformColor="#999999" /> - - - - diff --git a/app/src/main/res/layout/activity_settings_container.xml b/app/src/main/res/layout/activity_settings_container.xml index bb09b3f2..a8294827 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_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" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/folder_row.xml b/app/src/main/res/layout/folder_row.xml index 53bf25f5..b8873639 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/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"> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +