├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── ids.xml │ │ │ │ ├── ic_launcher_1_background.xml │ │ │ │ ├── ic_launcher_2_background.xml │ │ │ │ └── colors.xml │ │ │ ├── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ ├── provider_paths.xml │ │ │ │ ├── data_extraction_rules.xml │ │ │ │ └── edit_tags_motion_scene.xml │ │ │ ├── drawable │ │ │ │ ├── tags_divider.xml │ │ │ │ ├── bg_rounded_8.xml │ │ │ │ ├── bg_rounded_16dp.xml │ │ │ │ ├── bg_dialog_rounded_16dp.xml │ │ │ │ ├── bg_rounded_6_top.xml │ │ │ │ ├── bg_rounded_top_24dp.xml │ │ │ │ ├── ic_arrow_up.xml │ │ │ │ ├── ic_arrow_down.xml │ │ │ │ ├── ic_play.xml │ │ │ │ ├── ic_chevron_right_black_24dp.xml │ │ │ │ ├── ic_star_24.xml │ │ │ │ ├── ic_star.xml │ │ │ │ ├── ic_drag_handle.xml │ │ │ │ ├── ic_sort.xml │ │ │ │ ├── ic_list_add.xml │ │ │ │ ├── ic_baseline_add.xml │ │ │ │ ├── ic_arrow_right.xml │ │ │ │ ├── bg_rounded_black_transparent.xml │ │ │ │ ├── bg_rounded_black_transparent_small.xml │ │ │ │ ├── ic_delete.xml │ │ │ │ ├── ic_folder_action_add.xml │ │ │ │ ├── order_ascending.xml │ │ │ │ ├── order_descending.xml │ │ │ │ ├── ic_baseline_delete_24.xml │ │ │ │ ├── ic_folder_tree_node_add.xml │ │ │ │ ├── ripple_simple_bg.xml │ │ │ │ ├── ic_info.xml │ │ │ │ ├── ic_close.xml │ │ │ │ ├── ic_baseline_folder.xml │ │ │ │ ├── ic_folder_tree_node_navigate.xml │ │ │ │ ├── ic_menu_explore.xml │ │ │ │ ├── ic_baseline_info.xml │ │ │ │ ├── ic_push_pin.xml │ │ │ │ ├── ic_cloud_on.xml │ │ │ │ ├── ic_baseline_edit_24.xml │ │ │ │ ├── ic_vert_dots.xml │ │ │ │ ├── ic_baseline_favorite_24.xml │ │ │ │ ├── ic_edit_file.xml │ │ │ │ ├── ic_baseline_chooser_24.xml │ │ │ │ ├── ic_menu_navigate.xml │ │ │ │ ├── ic_folder_action_navigate.xml │ │ │ │ ├── ic_baseline_search_24.xml │ │ │ │ ├── ic_cloud_off.xml │ │ │ │ ├── ic_file.xml │ │ │ │ ├── radio_button_bg.xml │ │ │ │ ├── ic_file_flv.xml │ │ │ │ ├── ic_file_zip.xml │ │ │ │ ├── ic_file_txt.xml │ │ │ │ ├── ic_file_gif.xml │ │ │ │ ├── ic_file_avi.xml │ │ │ │ ├── ic_file_html.xml │ │ │ │ ├── ic_file_mkv.xml │ │ │ │ ├── ic_file_pdf.xml │ │ │ │ ├── ic_settings.xml │ │ │ │ ├── ic_file_rar.xml │ │ │ │ ├── ic_file_jpg.xml │ │ │ │ ├── ic_file_mp4.xml │ │ │ │ ├── ic_file_png.xml │ │ │ │ ├── ic_file_wav.xml │ │ │ │ ├── ic_file_mov.xml │ │ │ │ ├── ic_file_doc.xml │ │ │ │ ├── ic_file_xls.xml │ │ │ │ ├── ic_file_jpeg.xml │ │ │ │ ├── ic_file_wmv.xml │ │ │ │ ├── ic_file_bmp.xml │ │ │ │ ├── ic_file_ts.xml │ │ │ │ ├── ic_file_svg.xml │ │ │ │ ├── ic_file_mpg.xml │ │ │ │ ├── ic_file_mp3.xml │ │ │ │ ├── ic_file_3gp.xml │ │ │ │ ├── ic_file_wma.xml │ │ │ │ ├── ic_file_xlsx.xml │ │ │ │ ├── ic_file_docx.xml │ │ │ │ ├── ic_file_webm.xml │ │ │ │ ├── ic_dice.xml │ │ │ │ └── ic_launcher_2_foreground.xml │ │ │ ├── color │ │ │ │ ├── chip_text_color.xml │ │ │ │ └── bottom_navigation_item.xml │ │ │ ├── anim │ │ │ │ ├── fade_in.xml │ │ │ │ ├── fade_out.xml │ │ │ │ ├── bottom_fade_scale_in.xml │ │ │ │ └── bottom_fade_scale_out.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher_1.xml │ │ │ │ └── ic_launcher_2.xml │ │ │ ├── layout │ │ │ │ ├── item_tag.xml │ │ │ │ ├── popup_resources_tag_menu.xml │ │ │ │ ├── item_preview_plain_text.xml │ │ │ │ ├── item_toast.xml │ │ │ │ ├── layout_progress.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── popup_gallery_tag_menu.xml │ │ │ │ ├── item_view_folder_tree_device.xml │ │ │ │ ├── item_image.xml │ │ │ │ ├── popup_selected_resources_actions.xml │ │ │ │ ├── item_view_folder_tree_favorite.xml │ │ │ │ ├── item_boolean_preference.xml │ │ │ │ ├── fragment_settings.xml │ │ │ │ ├── dialog_roots_new.xml │ │ │ │ ├── dialog_sort.xml │ │ │ │ ├── fragment_folders.xml │ │ │ │ └── dialog_notification.xml │ │ │ ├── menu │ │ │ │ ├── menu_tags_screen.xml │ │ │ │ └── menu_bottom_navigation.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_baseline_share.xml │ │ ├── java │ │ │ └── dev │ │ │ │ └── arkbuilders │ │ │ │ └── navigator │ │ │ │ ├── analytics │ │ │ │ ├── settings │ │ │ │ │ ├── SettingsAnalytics.kt │ │ │ │ │ └── SettingsAnalyticsImpl.kt │ │ │ │ ├── folders │ │ │ │ │ ├── FoldersAnalytics.kt │ │ │ │ │ └── FoldersAnalyticsImpl.kt │ │ │ │ ├── gallery │ │ │ │ │ ├── GalleryAnalytics.kt │ │ │ │ │ └── GalleryAnalyticsImpl.kt │ │ │ │ ├── Utils.kt │ │ │ │ ├── resources │ │ │ │ │ ├── ResourcesAnalytics.kt │ │ │ │ │ └── ResourcesAnalyticsImpl.kt │ │ │ │ └── AnalyticsModule.kt │ │ │ │ ├── presentation │ │ │ │ ├── navigation │ │ │ │ │ ├── FragmentForwardAdd.kt │ │ │ │ │ ├── AppRouter.kt │ │ │ │ │ └── Screens.kt │ │ │ │ ├── utils │ │ │ │ │ ├── ContextUtils.kt │ │ │ │ │ ├── EditTextExt.kt │ │ │ │ │ ├── StringProvider.kt │ │ │ │ │ ├── ToastUtils.kt │ │ │ │ │ ├── extra │ │ │ │ │ │ ├── LinkExtraLoader.kt │ │ │ │ │ │ ├── DocumentExtraLoader.kt │ │ │ │ │ │ └── ExtraLoader.kt │ │ │ │ │ └── FullscreenHelper.kt │ │ │ │ ├── common │ │ │ │ │ └── CommonMvpView.kt │ │ │ │ ├── dialog │ │ │ │ │ ├── sort │ │ │ │ │ │ ├── SortDialogView.kt │ │ │ │ │ │ └── SortDialogPresenter.kt │ │ │ │ │ ├── edittags │ │ │ │ │ │ └── EditTagsDialogView.kt │ │ │ │ │ ├── rootsscan │ │ │ │ │ │ ├── RootsScanView.kt │ │ │ │ │ │ └── RootsScanDialogPresenter.kt │ │ │ │ │ ├── ExplainPermsDialog.kt │ │ │ │ │ ├── InfoDialogFragment.kt │ │ │ │ │ └── StorageExceptionDialogFragment.kt │ │ │ │ ├── screen │ │ │ │ │ ├── gallery │ │ │ │ │ │ ├── domain │ │ │ │ │ │ │ └── GalleryItem.kt │ │ │ │ │ │ └── pager │ │ │ │ │ │ │ └── PreviewPlainTextViewHolder.kt │ │ │ │ │ └── resources │ │ │ │ │ │ ├── adapter │ │ │ │ │ │ ├── ResourceDiffUtilCallback.kt │ │ │ │ │ │ └── ResourcesRVAdapter.kt │ │ │ │ │ │ └── ResourcesView.kt │ │ │ │ ├── view │ │ │ │ │ ├── StackedToastsRecyclerView.kt │ │ │ │ │ ├── KeyListenEditText.kt │ │ │ │ │ ├── DepthPageTransformer.kt │ │ │ │ │ ├── LoadingTextView.kt │ │ │ │ │ └── UserSwitchMaterial.kt │ │ │ │ └── App.kt │ │ │ │ ├── data │ │ │ │ ├── utils │ │ │ │ │ ├── DevicePathsExtractor.kt │ │ │ │ │ ├── Popularity.kt │ │ │ │ │ ├── LogTags.kt │ │ │ │ │ ├── DevicePathsExtractorImpl.kt │ │ │ │ │ └── PathExt.kt │ │ │ │ ├── stats │ │ │ │ │ ├── StatsStorage.kt │ │ │ │ │ ├── category │ │ │ │ │ │ ├── StatsCategoryStorage.kt │ │ │ │ │ │ ├── TagQueriedTSStorage.kt │ │ │ │ │ │ ├── TagLabeledTSStorage.kt │ │ │ │ │ │ └── TagQueriedNStorage.kt │ │ │ │ │ ├── StatsStorageRepo.kt │ │ │ │ │ └── AggregatedStatsStorage.kt │ │ │ │ └── preferences │ │ │ │ │ └── Preferences.kt │ │ │ │ └── di │ │ │ │ └── modules │ │ │ │ ├── CiceroneModule.kt │ │ │ │ ├── DispatcherModule.kt │ │ │ │ └── AppModule.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── dev │ │ │ └── arkbuilders │ │ │ └── navigator │ │ │ ├── stub │ │ │ ├── StatsStorageStub.kt │ │ │ ├── TagsStorageStub.kt │ │ │ ├── MetadataProcessorStub.kt │ │ │ ├── ResourceIndexStub.kt │ │ │ └── TestData.kt │ │ │ └── data │ │ │ └── utils │ │ │ ├── DevicePathsExtractorTest.kt │ │ │ └── PathExtKtTest.kt │ └── androidTest │ │ └── java │ │ └── dev │ │ └── arkbuilders │ │ └── navigator │ │ └── ExampleInstrumentedTest.kt ├── app-scripts │ └── pre-commit-unix └── proguard-rules.pro ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── release.yml │ └── sonar_analysis.yml ├── .gitignore ├── .editorconfig ├── LICENSE ├── gradle.properties ├── CONTRIBUTING.md └── gradlew.bat /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /libs 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "ARK Navigator" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARK-Builders/ARK-Navigator/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_1_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_2_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1C7AE8 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | .gradle 4 | /local.properties 5 | .DS_Store 6 | /build 7 | /buildSrc/build/ 8 | *.class 9 | /captures 10 | .externalNativeBuild 11 | .cxx 12 | *.tab 13 | *.tab.values.at 14 | misc.xml 15 | /htmlReport 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tags_divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_rounded_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/analytics/settings/SettingsAnalytics.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.analytics.settings 2 | 3 | interface SettingsAnalytics { 4 | fun trackScreen() 5 | fun trackBooleanPref(name: String, enabled: Boolean) 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/chip_text_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_rounded_16dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jan 08 19:45:49 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/navigation/FragmentForwardAdd.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.navigation 2 | 3 | import ru.terrakok.cicerone.Screen 4 | import ru.terrakok.cicerone.commands.Command 5 | 6 | class FragmentForwardAdd(val screen: Screen) : Command 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/bottom_navigation_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_dialog_rounded_16dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/analytics/folders/FoldersAnalytics.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.analytics.folders 2 | 3 | interface FoldersAnalytics { 4 | fun trackScreen() 5 | fun trackRootOpen() 6 | fun trackFavOpen() 7 | fun trackRootAdded() 8 | fun trackFavAdded() 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_rounded_6_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## :rocket: Summary 2 | Describe things you did in this Pull Request 3 | Issue: 4 | 5 | ## :framed_picture: Screenshots: 6 | Provide screenshots to make it visible to reviewer if possible (Optional) 7 | 8 | | Before | After | 9 | | :---: | :---: | 10 | | Screenshot_1 | Screenshot_2 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_rounded_top_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/utils/DevicePathsExtractor.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.utils 2 | 3 | import java.nio.file.Path 4 | 5 | enum class Sorting { 6 | DEFAULT, NAME, SIZE, LAST_MODIFIED, TYPE 7 | } 8 | interface DevicePathsExtractor { 9 | fun listDevices(): List 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_tag.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/navigation/AppRouter.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.navigation 2 | 3 | import ru.terrakok.cicerone.Router 4 | import ru.terrakok.cicerone.Screen 5 | 6 | class AppRouter : Router() { 7 | fun navigateToFragmentUsingAdd(screen: Screen) { 8 | executeCommands(FragmentForwardAdd(screen)) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_up.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/utils/ContextUtils.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.utils 2 | 3 | import android.content.Context 4 | import android.util.TypedValue 5 | 6 | fun Context.dpToPx(dp: Float): Float = 7 | TypedValue.applyDimension( 8 | TypedValue.COMPLEX_UNIT_DIP, 9 | dp, 10 | this.resources.displayMetrics 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chevron_right_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/analytics/gallery/GalleryAnalytics.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.analytics.gallery 2 | 3 | interface GalleryAnalytics { 4 | fun trackScreen() 5 | fun trackResOpen() 6 | fun trackResShare() 7 | fun trackResInfo() 8 | fun trackResEdit() 9 | fun trackResRemove() 10 | fun trackTagSelect() 11 | fun trackTagRemove() 12 | fun trackTagsEdit() 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/utils/Popularity.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.utils 2 | 3 | class Popularity { 4 | companion object { 5 | fun calculate(elements: List): Map { 6 | val result = mutableMapOf() 7 | 8 | elements.forEach { result[it] = (result[it] ?: 0) + 1 } 9 | 10 | return result.toMap() 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_drag_handle.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sort.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/common/CommonMvpView.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.common 2 | 3 | import moxy.MvpView 4 | import moxy.viewstate.strategy.SkipStrategy 5 | import moxy.viewstate.strategy.StateStrategyType 6 | import java.nio.file.Path 7 | 8 | interface CommonMvpView : MvpView { 9 | @StateStrategyType(SkipStrategy::class) 10 | fun toastIndexFailedPath(path: Path) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_list_add.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_add.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_rounded_black_transparent.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_rounded_black_transparent_small.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_action_add.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/order_ascending.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/order_descending.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_delete_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_tree_node_add.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ripple_simple_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_folder.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_tree_node_navigate.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/popup_resources_tag_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_explore.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_info.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_push_pin.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/analytics/Utils.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.analytics 2 | 3 | import org.matomo.sdk.Tracker 4 | import org.matomo.sdk.extra.TrackHelper 5 | 6 | fun Tracker.trackScreen(build: TrackHelper.() -> TrackHelper.Screen) { 7 | val matomoTracker = this 8 | build(TrackHelper.track()).with(matomoTracker) 9 | } 10 | 11 | fun Tracker.trackEvent(build: TrackHelper.() -> TrackHelper.EventBuilder) { 12 | val matomoTracker = this 13 | build(TrackHelper.track()).with(matomoTracker) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/dialog/sort/SortDialogView.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.dialog.sort 2 | 3 | import moxy.MvpView 4 | import moxy.viewstate.strategy.AddToEndSingleStrategy 5 | import moxy.viewstate.strategy.StateStrategyType 6 | import dev.arkbuilders.navigator.data.utils.Sorting 7 | 8 | @StateStrategyType(AddToEndSingleStrategy::class) 9 | interface SortDialogView : MvpView { 10 | fun init(sorting: Sorting, ascending: Boolean, sortByScoresEnabled: Boolean) 11 | fun closeDialog() 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/res/anim/bottom_fade_scale_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cloud_on.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/anim/bottom_fade_scale_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/domain/GalleryItem.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.screen.gallery.domain 2 | 3 | import dev.arkbuilders.arklib.data.index.Resource 4 | import dev.arkbuilders.arklib.data.meta.Metadata 5 | import dev.arkbuilders.arklib.data.preview.PreviewLocator 6 | import java.nio.file.Path 7 | 8 | data class GalleryItem( 9 | val resource: Resource, 10 | val preview: PreviewLocator, 11 | val metadata: Metadata, 12 | val path: Path 13 | ) { 14 | fun id() = resource.id 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_edit_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_vert_dots.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_favorite_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit_file.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_chooser_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_navigate.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_action_navigate.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_search_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/view/StackedToastsRecyclerView.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.MotionEvent 6 | import androidx.recyclerview.widget.RecyclerView 7 | 8 | class StackedToastsRecyclerView @JvmOverloads constructor( 9 | context: Context, 10 | attrs: AttributeSet? = null, 11 | defStyleAttr: Int = 0 12 | ) : RecyclerView(context, attrs, defStyleAttr) { 13 | 14 | override fun onInterceptTouchEvent(e: MotionEvent?) = false 15 | 16 | override fun onTouchEvent(e: MotionEvent?) = false 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_preview_plain_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/app-scripts/pre-commit-unix: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "*********************************************************" 3 | echo "Running git pre-commit hook, ktlintCheck in progress..." 4 | echo "*********************************************************" 5 | 6 | ./gradlew ktlintCheck 7 | 8 | status=$? 9 | 10 | if [ "$status" = 0 ] ; then 11 | echo "Static analysis found no problems." 12 | exit 0 13 | else 14 | echo "*********************************************************" 15 | echo 1>&2 "ktlintCheck found violations it could not fix." 16 | echo "Run ./gradlew ktlintFormat to fix formatting related issues..." 17 | echo "*********************************************************" 18 | exit 1 19 | fi 20 | -------------------------------------------------------------------------------- /app/src/test/java/dev/arkbuilders/navigator/stub/StatsStorageStub.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.stub 2 | 3 | import dev.arkbuilders.arklib.data.stats.StatsEvent 4 | import dev.arkbuilders.navigator.data.stats.StatsStorage 5 | import dev.arkbuilders.arklib.user.tags.Tag 6 | 7 | class StatsStorageStub : StatsStorage { 8 | override suspend fun init() {} 9 | 10 | override fun handleEvent(event: StatsEvent) {} 11 | 12 | override fun statsTagLabeledAmount(): Map = emptyMap() 13 | override fun statsTagQueriedAmount(): Map = emptyMap() 14 | 15 | override fun statsTagQueriedTS(): Map = emptyMap() 16 | override fun statsTagLabeledTS(): Map = emptyMap() 17 | } 18 | -------------------------------------------------------------------------------- /app/src/test/java/dev/arkbuilders/navigator/stub/TagsStorageStub.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.stub 2 | 3 | import dev.arkbuilders.arklib.ResourceId 4 | import dev.arkbuilders.arklib.user.tags.TagStorage 5 | import dev.arkbuilders.arklib.user.tags.Tags 6 | 7 | class TagsStorageStub : TagStorage { 8 | private val tagsById = TestData.tagsById().toMutableMap() 9 | 10 | override fun getValue(id: ResourceId) = tagsById[id]!! 11 | 12 | override suspend fun persist() {} 13 | 14 | override fun remove(id: ResourceId) { 15 | tagsById.remove(id) 16 | } 17 | 18 | override fun setValue( 19 | id: ResourceId, 20 | value: Tags 21 | ) { 22 | tagsById[id] = value 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | #717171 7 | #50717171 8 | #D3D3D3 9 | #E5E4E2 10 | #FFFFFF 11 | #000000 12 | #A6000000 13 | #99000000 14 | #ADD8E6 15 | #BFFFFFFF 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cloud_off.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/stats/StatsStorage.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.stats 2 | 3 | import dev.arkbuilders.arklib.data.stats.StatsEvent 4 | import dev.arkbuilders.arklib.user.tags.Tag 5 | 6 | interface StatsStorage { 7 | suspend fun init() 8 | fun handleEvent(event: StatsEvent) 9 | fun statsTagLabeledAmount(): Map 10 | fun statsTagQueriedAmount(): Map 11 | fun statsTagQueriedTS(): Map 12 | fun statsTagLabeledTS(): Map 13 | 14 | companion object { 15 | val TAGS_USAGE_EVENTS = listOf( 16 | StatsEvent.TagsChanged::class.java, 17 | StatsEvent.PlainTagUsed::class.java, 18 | StatsEvent.KindTagUsed::class.java 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/utils/EditTextExt.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.utils 2 | 3 | import android.view.inputmethod.InputMethodManager 4 | import android.widget.EditText 5 | import androidx.core.content.ContextCompat.getSystemService 6 | 7 | fun EditText.showKeyboard() { 8 | val imm = getSystemService(context, InputMethodManager::class.java) 9 | imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) 10 | } 11 | 12 | fun EditText.closeKeyboard() { 13 | val imm = getSystemService(context, InputMethodManager::class.java) 14 | imm?.hideSoftInputFromWindow(this.windowToken, 0) 15 | } 16 | 17 | fun EditText.placeCursorToEnd() { 18 | requestFocus() 19 | post { 20 | setSelection(length()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/utils/LogTags.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.utils 2 | 3 | object LogTags { 4 | const val MAIN: String = "main" 5 | 6 | const val PERMISSIONS: String = "permissions" 7 | 8 | const val RESOURCES_SCREEN: String = "resources-screen" 9 | 10 | const val GALLERY_SCREEN: String = "gallery-screen" 11 | 12 | const val FOLDERS_SCREEN: String = "folders-screen" 13 | 14 | const val SETTINGS_SCREEN: String = "settings-screen" 15 | 16 | const val FOLDERS_TREE: String = "folders-tree" 17 | 18 | const val TAGS_SELECTOR: String = "tags-selector" 19 | 20 | const val TAGS_STORAGE: String = "tags-storage" 21 | 22 | const val SCORES_STORAGE: String = "scores-storage" 23 | 24 | const val FILES: String = "files" 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/dialog/edittags/EditTagsDialogView.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.dialog.edittags 2 | 3 | import moxy.MvpView 4 | import moxy.viewstate.strategy.AddToEndSingleStrategy 5 | import moxy.viewstate.strategy.SkipStrategy 6 | import moxy.viewstate.strategy.StateStrategyType 7 | import dev.arkbuilders.arklib.user.tags.Tag 8 | import dev.arkbuilders.arklib.user.tags.Tags 9 | 10 | @StateStrategyType(AddToEndSingleStrategy::class) 11 | interface EditTagsDialogView : MvpView { 12 | fun init() 13 | fun showKeyboardAndView() 14 | fun hideSortingBtn() 15 | fun setQuickTags(tags: List) 16 | fun setResourceTags(tags: Tags) 17 | fun setInput(input: String) 18 | 19 | @StateStrategyType(SkipStrategy::class) 20 | fun dismissDialog() 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_tags_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | 17 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/androidTest/java/dev/arkbuilders/navigator/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import junit.framework.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("dev.arkbuilders.navigator", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/analytics/settings/SettingsAnalyticsImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.analytics.settings 2 | 3 | import dev.arkbuilders.navigator.analytics.trackEvent 4 | import dev.arkbuilders.navigator.analytics.trackScreen 5 | import org.matomo.sdk.Tracker 6 | 7 | class SettingsAnalyticsImpl( 8 | private val matomoTracker: Tracker 9 | ) : SettingsAnalytics { 10 | override fun trackScreen() = matomoTracker.trackScreen { screen(SCREEN_NAME) } 11 | 12 | override fun trackBooleanPref(name: String, enabled: Boolean) { 13 | val enabledStr = if (enabled) "enabled" else "disabled" 14 | matomoTracker 15 | .trackEvent { event(SCREEN_NAME, "$name is $enabledStr") } 16 | } 17 | 18 | companion object { 19 | private const val SCREEN_NAME = "Settings screen" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/di/modules/CiceroneModule.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.di.modules 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import javax.inject.Singleton 6 | import ru.terrakok.cicerone.Cicerone 7 | import ru.terrakok.cicerone.NavigatorHolder 8 | import dev.arkbuilders.navigator.presentation.navigation.AppRouter 9 | 10 | @Module 11 | class CiceroneModule { 12 | 13 | @Singleton 14 | @Provides 15 | fun cicerone(): Cicerone { 16 | return Cicerone.create(AppRouter()) 17 | } 18 | 19 | @Provides 20 | fun navigationHolder(cicerone: Cicerone): NavigatorHolder { 21 | return cicerone.navigatorHolder 22 | } 23 | 24 | @Provides 25 | fun router(cicerone: Cicerone): AppRouter { 26 | return cicerone.router 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/view/KeyListenEditText.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.KeyEvent 6 | import com.google.android.material.textfield.TextInputEditText 7 | 8 | class KeyListenEditText(context: Context, attrs: AttributeSet?) : 9 | TextInputEditText(context, attrs) { 10 | 11 | var onBackPressedListener: (() -> Boolean)? = null 12 | 13 | override fun onKeyPreIme(keyCode: Int, event: KeyEvent?): Boolean { 14 | if (keyCode == KeyEvent.KEYCODE_BACK && 15 | event?.action == KeyEvent.ACTION_DOWN 16 | ) { 17 | onBackPressedListener?.let { 18 | return it() 19 | } 20 | } 21 | return super.onKeyPreIme(keyCode, event) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/radio_button_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/analytics/resources/ResourcesAnalytics.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.analytics.resources 2 | 3 | import dev.arkbuilders.arklib.data.storage.StorageException 4 | import dev.arkbuilders.components.tagselector.QueryMode 5 | import dev.arkbuilders.components.tagselector.TagsSorting 6 | import dev.arkbuilders.navigator.data.utils.Sorting 7 | 8 | interface ResourcesAnalytics { 9 | fun trackScreen() 10 | fun trackResClick() 11 | fun trackMoveSelectedRes() 12 | fun trackCopySelectedRes() 13 | fun trackRemoveSelectedRes() 14 | fun trackShareSelectedRes() 15 | fun trackResShuffle() 16 | fun trackTagSortCriteria(tagsSorting: TagsSorting) 17 | fun trackResSortCriteria(sorting: Sorting) 18 | fun trackQueryModeChanged(queryMode: QueryMode) 19 | fun trackStorageProvideException(exception: StorageException) 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/screen/resources/adapter/ResourceDiffUtilCallback.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.screen.resources.adapter 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import dev.arkbuilders.arklib.ResourceId 5 | 6 | class ResourceDiffUtilCallback( 7 | private val oldItems: List, 8 | private val newItems: List 9 | ) : DiffUtil.Callback() { 10 | override fun getOldListSize(): Int = oldItems.size 11 | 12 | override fun getNewListSize(): Int = newItems.size 13 | 14 | override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean { 15 | return oldItems[oldPos] == newItems[newPos] 16 | } 17 | 18 | // due to content-addressing, `id1 = id2` means `content1 = content2` 19 | override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean = 20 | areItemsTheSame(oldPos, newPos) 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/dialog/rootsscan/RootsScanView.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.dialog.rootsscan 2 | 3 | import moxy.MvpView 4 | import moxy.viewstate.strategy.AddToEndSingleStrategy 5 | import moxy.viewstate.strategy.OneExecutionStateStrategy 6 | import moxy.viewstate.strategy.StateStrategyType 7 | import java.nio.file.Path 8 | 9 | @StateStrategyType(AddToEndSingleStrategy::class) 10 | interface RootsScanView : MvpView { 11 | fun init() 12 | fun startScan() 13 | fun scanCompleted(foundRoots: Int) 14 | fun setProgress(foundRoots: Int) 15 | 16 | @StateStrategyType(OneExecutionStateStrategy::class) 17 | fun notifyRootsFound(roots: List) 18 | 19 | @StateStrategyType(OneExecutionStateStrategy::class) 20 | fun toastFolderSkip(folder: Path) 21 | 22 | @StateStrategyType(OneExecutionStateStrategy::class) 23 | fun closeDialog() 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_bottom_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 16 | 17 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_toast.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/utils/DevicePathsExtractorImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.utils 2 | 3 | import dev.arkbuilders.navigator.presentation.App 4 | import java.nio.file.Path 5 | import javax.inject.Inject 6 | 7 | class DevicePathsExtractorImpl @Inject constructor( 8 | private val appInstance: App 9 | ) : DevicePathsExtractor { 10 | 11 | override fun listDevices(): List = 12 | appInstance 13 | .getExternalFilesDirs(null) 14 | .toList() 15 | .filterNotNull() 16 | .filter { it.exists() } 17 | .map { 18 | it.toPath().toRealPath() 19 | .takeWhile { part -> 20 | part != ANDROID_DIRECTORY 21 | } 22 | .fold(ROOT_PATH) { parent, child -> 23 | parent.resolve(child) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | # noinspection EditorConfigKeyCorrectness 9 | [*] 10 | 11 | indent_style = space 12 | indent_size = 4 13 | tab_width = 4 14 | 15 | disabled_rules=import-ordering 16 | 17 | no-unused-imports = true 18 | 19 | # https://stackoverflow.com/questions/147454/why-is-using-a-wild-card-with-a-java-import-statement-bad 20 | no-wildcard-import = true 21 | 22 | # https://softwareengineering.stackexchange.com/questions/604/is-the-80-character-limit-still-relevant-in-times-of-widescreen-monitors 23 | max_line_length = 85 24 | 25 | # We recommend you to keep these unchanged 26 | end_of_line = lf 27 | charset = utf-8 28 | trim_trailing_whitespace = true 29 | insert_final_newline = true 30 | 31 | [*.md] 32 | trim_trailing_whitespace = false 33 | 34 | [**/test/**.kt] 35 | max_line_length=off 36 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/pager/PreviewPlainTextViewHolder.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.screen.gallery.pager 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.core.view.GestureDetectorCompat 5 | import androidx.recyclerview.widget.RecyclerView 6 | import dev.arkbuilders.navigator.databinding.ItemPreviewPlainTextBinding 7 | 8 | @SuppressLint("ClickableViewAccessibility") 9 | class PreviewPlainTextViewHolder( 10 | private val binding: ItemPreviewPlainTextBinding, 11 | private val detector: GestureDetectorCompat 12 | ) : RecyclerView.ViewHolder(binding.root) { 13 | var pos = -1 14 | 15 | init { 16 | binding.tvContent.setOnTouchListener { view, event -> 17 | return@setOnTouchListener detector.onTouchEvent(event) 18 | } 19 | } 20 | 21 | fun setContent(text: String) = with(binding) { 22 | tvContent.text = text 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_flv.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_baseline_share.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release the app 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | environment: Development 12 | env: 13 | ACRA_LOGIN: ${{ secrets.ACRARIUM_BASIC_AUTH_LOGIN }} 14 | ACRA_PASS: ${{ secrets.ACRARIUM_BASIC_AUTH_PASSWORD }} 15 | ACRA_URI: ${{ secrets.ACRARIUM_URI }} 16 | BRANCH_NAME: ${{ github.ref_name }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up JDK 17 21 | uses: actions/setup-java@v4 22 | with: 23 | java-version: '17' 24 | distribution: 'adopt' 25 | 26 | - name: Validate Gradle wrapper 27 | uses: gradle/actions/wrapper-validation@v3 28 | 29 | - name: Build Release APK 30 | run: ./gradlew assembleRelease 31 | 32 | - name: Release 33 | uses: ncipollo/release-action@v1 34 | with: 35 | artifacts: "./app/build/outputs/apk/release/*.apk" 36 | token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /app/src/test/java/dev/arkbuilders/navigator/stub/MetadataProcessorStub.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.stub 2 | 3 | import dev.arkbuilders.arklib.ResourceId 4 | import dev.arkbuilders.arklib.data.meta.Metadata 5 | import dev.arkbuilders.arklib.data.meta.MetadataUpdate 6 | import dev.arkbuilders.arklib.data.processor.RootProcessor 7 | 8 | class MetadataProcessorStub : RootProcessor() { 9 | private val metaById: MutableMap = mapOf( 10 | R1 to Metadata.PlainText(), 11 | R2 to Metadata.Image(), 12 | R3 to Metadata.Video(1, 1, 10), 13 | R4 to Metadata.Archive() 14 | ).toMutableMap() 15 | 16 | override suspend fun init() {} 17 | 18 | override fun retrieve(id: ResourceId): Result = 19 | metaById.mapValues { Result.success(it.value) } 20 | .getOrDefault(id, Result.failure(IllegalArgumentException())) 21 | 22 | override fun forget(id: ResourceId) { 23 | metaById.remove(id) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/utils/StringProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.utils 2 | 3 | import android.content.Context 4 | import androidx.annotation.StringRes 5 | import dev.arkbuilders.arklib.data.meta.Kind 6 | import dev.arkbuilders.navigator.R 7 | 8 | class StringProvider(private val context: Context) { 9 | fun getString(@StringRes stringResId: Int): String { 10 | return context.getString(stringResId) 11 | } 12 | 13 | fun kindToString(kind: Kind) = when (kind) { 14 | Kind.IMAGE -> context.getString(R.string.kind_image) 15 | Kind.VIDEO -> context.getString(R.string.kind_video) 16 | Kind.DOCUMENT -> context.getString(R.string.kind_document) 17 | Kind.LINK -> context.getString(R.string.kind_link) 18 | Kind.ARCHIVE -> context.getString(R.string.kind_archive) 19 | Kind.PLAINTEXT -> context.getString(R.string.kind_plain_text) 20 | Kind.UNKNOWN -> context.getString(R.string.kind_unknown) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/di/modules/DispatcherModule.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.di.modules 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import kotlinx.coroutines.Dispatchers 7 | import javax.inject.Qualifier 8 | 9 | @Module 10 | class DispatcherModule { 11 | 12 | @DefaultDispatcher 13 | @Provides 14 | fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default 15 | 16 | @IoDispatcher 17 | @Provides 18 | fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO 19 | 20 | @MainDispatcher 21 | @Provides 22 | fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main 23 | } 24 | 25 | @Retention(AnnotationRetention.BINARY) 26 | @Qualifier 27 | annotation class DefaultDispatcher 28 | 29 | @Retention(AnnotationRetention.BINARY) 30 | @Qualifier 31 | annotation class IoDispatcher 32 | 33 | @Retention(AnnotationRetention.BINARY) 34 | @Qualifier 35 | annotation class MainDispatcher 36 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_zip.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 18 | 19 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_txt.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -dontwarn org.slf4j.impl.StaticLoggerBinder 24 | -dontwarn javax.xml.stream.XMLResolver 25 | 26 | -keep class dev.arkbuilders.arklib.** { *; } 27 | -keep class wseemann.media.FFmpegMediaMetadataRetriever.** { *; } 28 | -keep @kotlinx.serialization.Serializable class * {*;} 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_gif.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-2022 Kirill Taran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/utils/ToastUtils.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.utils 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import androidx.annotation.StringRes 6 | import androidx.fragment.app.Fragment 7 | import dev.arkbuilders.navigator.R 8 | import java.nio.file.Path 9 | 10 | fun Context.toast( 11 | @StringRes stringId: Int, 12 | vararg args: Any, 13 | moreTime: Boolean = false 14 | ) { 15 | val duration = if (moreTime) Toast.LENGTH_LONG else Toast.LENGTH_SHORT 16 | Toast.makeText(this, getString(stringId, *args), duration).show() 17 | } 18 | 19 | fun Context.toastFailedPaths(failedPaths: List) { 20 | if (failedPaths.isEmpty()) return 21 | val list = failedPaths.joinToString("\n") 22 | toast(R.string.toast_failed_paths, list, moreTime = true) 23 | } 24 | 25 | fun Fragment.toast( 26 | @StringRes stringId: Int, 27 | vararg args: Any, 28 | moreTime: Boolean = false 29 | ) = requireContext().toast(stringId, *args, moreTime = moreTime) 30 | 31 | fun Fragment.toastFailedPaths( 32 | failedPaths: List 33 | ) = requireContext().toastFailedPaths(failedPaths) 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_avi.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_html.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/dialog/sort/SortDialogPresenter.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.dialog.sort 2 | 3 | import dev.arkbuilders.navigator.data.preferences.PreferenceKey 4 | import dev.arkbuilders.navigator.data.preferences.Preferences 5 | import dev.arkbuilders.navigator.data.utils.Sorting 6 | import kotlinx.coroutines.launch 7 | import moxy.MvpPresenter 8 | import moxy.presenterScope 9 | import javax.inject.Inject 10 | 11 | class SortDialogPresenter : MvpPresenter() { 12 | @Inject 13 | lateinit var preferences: Preferences 14 | 15 | override fun onFirstViewAttach() { 16 | presenterScope.launch { 17 | val sorting = Sorting.values()[preferences.get(PreferenceKey.Sorting)] 18 | val ascending = preferences.get(PreferenceKey.IsSortingAscending) 19 | val sortByScores = preferences.get(PreferenceKey.SortByScores) 20 | viewState.init(sorting, ascending, sortByScores) 21 | } 22 | } 23 | 24 | fun onSortingSelected(sorting: Sorting) = presenterScope.launch { 25 | preferences.set(PreferenceKey.Sorting, sorting.ordinal) 26 | viewState.closeDialog() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/analytics/folders/FoldersAnalyticsImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.analytics.folders 2 | 3 | import android.content.Context 4 | import dev.arkbuilders.navigator.analytics.trackEvent 5 | import dev.arkbuilders.navigator.analytics.trackScreen 6 | import org.matomo.sdk.Tracker 7 | import javax.inject.Inject 8 | 9 | class FoldersAnalyticsImpl @Inject constructor( 10 | private val matomoTracker: Tracker, 11 | private val context: Context 12 | ) : FoldersAnalytics { 13 | 14 | override fun trackScreen() = matomoTracker 15 | .trackScreen { screen(SCREEN_NAME) } 16 | 17 | override fun trackRootOpen() = matomoTracker.trackScreenEvent("Root opened") 18 | 19 | override fun trackFavOpen() = matomoTracker.trackScreenEvent("Fav opened") 20 | 21 | override fun trackRootAdded() = matomoTracker.trackScreenEvent("Root added") 22 | 23 | override fun trackFavAdded() = matomoTracker.trackScreenEvent("Fav added") 24 | 25 | private fun Tracker.trackScreenEvent(action: String) = this.trackEvent { 26 | event(SCREEN_NAME, action) 27 | } 28 | 29 | companion object { 30 | private const val SCREEN_NAME = "Folders screen" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_mkv.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/test/java/dev/arkbuilders/navigator/stub/ResourceIndexStub.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.stub 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.MutableSharedFlow 5 | import dev.arkbuilders.arklib.ResourceId 6 | import dev.arkbuilders.arklib.data.index.Resource 7 | import dev.arkbuilders.arklib.data.index.ResourceIndex 8 | import dev.arkbuilders.arklib.data.index.ResourceUpdates 9 | import dev.arkbuilders.arklib.data.index.RootIndex 10 | 11 | import java.nio.file.Path 12 | import kotlin.io.path.Path 13 | 14 | class ResourceIndexStub : ResourceIndex { 15 | private val resources = TestData.resourceById().toMutableMap() 16 | 17 | override val roots: Set = setOf() 18 | 19 | override val updates: Flow = 20 | MutableSharedFlow() 21 | 22 | override suspend fun updateAll() {} 23 | 24 | override fun allResources(): Map = 25 | resources.toMap() 26 | 27 | override fun getResource(id: ResourceId): Resource? = 28 | resources[id] 29 | 30 | override fun allPaths(): Map = 31 | resources.mapValues { Path("") } 32 | 33 | override fun getPath(id: ResourceId): Path = 34 | Path("") 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/utils/extra/LinkExtraLoader.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.utils.extra 2 | 3 | import android.widget.TextView 4 | import dev.arkbuilders.navigator.R 5 | import dev.arkbuilders.arklib.data.meta.Metadata 6 | import dev.arkbuilders.navigator.presentation.utils.textOrGone 7 | 8 | object LinkExtraLoader { 9 | fun load(link: Metadata.Link, titleTV: TextView, verbose: Boolean) { 10 | if (!verbose) return 11 | titleTV.textOrGone(link.title) 12 | } 13 | 14 | fun loadWithLabel( 15 | link: Metadata.Link, 16 | titleTV: TextView, 17 | descriptionTV: TextView, 18 | linkTv: TextView 19 | ) { 20 | titleTV.textOrGone( 21 | titleTV.context.getString( 22 | R.string.link_title_label, 23 | link.title 24 | ) 25 | ) 26 | link.description?.let { 27 | descriptionTV.textOrGone( 28 | descriptionTV.context.getString( 29 | R.string.link_description_label, 30 | it 31 | ) 32 | ) 33 | } 34 | linkTv.textOrGone(linkTv.context.getString(R.string.link_label)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_pdf.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/utils/extra/DocumentExtraLoader.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.utils.extra 2 | 3 | import android.widget.TextView 4 | import dev.arkbuilders.navigator.R 5 | import dev.arkbuilders.arklib.data.meta.Metadata 6 | import dev.arkbuilders.navigator.presentation.utils.textOrGone 7 | 8 | object DocumentExtraLoader { 9 | fun load(document: Metadata.Document, pagesTV: TextView, verbose: Boolean) { 10 | val pages = document.pages 11 | if (pages != null) { 12 | val label = when { 13 | verbose -> { 14 | if (pages == 1) { 15 | "$pages page" 16 | } else { 17 | "$pages pages" 18 | } 19 | } 20 | else -> "$pages" 21 | } 22 | pagesTV.textOrGone(label) 23 | } 24 | } 25 | 26 | fun loadWithLabel( 27 | document: Metadata.Document, 28 | tvPageNumber: TextView 29 | ) { 30 | val pages = document.pages 31 | if (pages != null) { 32 | tvPageNumber.textOrGone( 33 | tvPageNumber.context.getString(R.string.doc_page_no_label, pages) 34 | ) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_rar.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_jpg.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_mp4.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_png.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/sonar_analysis.yml: -------------------------------------------------------------------------------- 1 | name: Analyze the app 2 | 3 | on: 4 | # Trigger analysis when pushing in master or pull requests, and when creating 5 | # a pull request. 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | types: [opened, synchronize, reopened] 11 | jobs: 12 | build: 13 | name: SonarQube Analysis 14 | runs-on: ubuntu-latest 15 | permissions: read-all 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 20 | - name: Set up JDK 17 21 | uses: actions/setup-java@v4 22 | with: 23 | java-version: '17' 24 | distribution: 'adopt' 25 | - name: Cache SonarQube packages 26 | uses: actions/cache@v4 27 | with: 28 | path: ~/.sonar/cache 29 | key: ${{ runner.os }}-sonar 30 | restore-keys: ${{ runner.os }}-sonar 31 | - name: Cache Gradle packages 32 | uses: actions/cache@v4 33 | with: 34 | path: ~/.gradle/caches 35 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 36 | restore-keys: ${{ runner.os }}-gradle 37 | - name: Build and analyze 38 | env: 39 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 40 | run: ./gradlew sonar --info 41 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_wav.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_mov.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_doc.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_xls.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | android.defaults.buildfeatures.buildconfig=true 23 | android.nonTransitiveRClass=false 24 | android.nonFinalResIds=false 25 | android.enableD8.desugaring = true 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_jpeg.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_wmv.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/view/DepthPageTransformer.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.view 2 | 3 | import android.view.View 4 | import androidx.viewpager2.widget.ViewPager2 5 | import kotlin.math.abs 6 | 7 | private const val MIN_SCALE = 0.75f 8 | 9 | class DepthPageTransformer() : ViewPager2.PageTransformer { 10 | 11 | override fun transformPage(view: View, position: Float) { 12 | view.apply { 13 | val pageWidth = width 14 | when { 15 | position < -1 -> { 16 | alpha = 0f 17 | } 18 | position <= 0 -> { 19 | alpha = 1f 20 | translationX = 0f 21 | translationZ = 0f 22 | scaleX = 1f 23 | scaleY = 1f 24 | } 25 | position <= 1 -> { 26 | alpha = 1 - position 27 | 28 | translationX = pageWidth * -position 29 | translationZ = -1f 30 | 31 | val scaleFactor = 32 | (MIN_SCALE + (1 - MIN_SCALE) * (1 - abs(position))) 33 | scaleX = scaleFactor 34 | scaleY = scaleFactor 35 | } 36 | else -> { 37 | alpha = 0f 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/test/java/dev/arkbuilders/navigator/stub/TestData.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.stub 2 | 3 | import dev.arkbuilders.arklib.ResourceId 4 | import dev.arkbuilders.arklib.data.index.Resource 5 | import java.nio.file.attribute.FileTime 6 | import java.util.Date 7 | 8 | object TestData { 9 | fun resourceById() = mapOf( 10 | R1 to Resource( 11 | R1, 12 | "Resource1", 13 | ".jpg", 14 | fileTime() 15 | ), 16 | R2 to Resource( 17 | R2, 18 | "Resource2", 19 | ".jpg", 20 | fileTime() 21 | ), 22 | R3 to Resource( 23 | R3, 24 | "Resource3", 25 | ".jpg", 26 | fileTime() 27 | ), 28 | R4 to Resource( 29 | R4, 30 | "Resource4", 31 | ".odt", 32 | fileTime() 33 | ) 34 | ) 35 | 36 | fun tagsById() = mapOf( 37 | R1 to setOf(TAG1, TAG2), 38 | R2 to setOf(TAG2), 39 | R3 to setOf(), 40 | R4 to setOf(TAG3, TAG4) 41 | ) 42 | 43 | private fun fileTime() = FileTime.from(Date().toInstant()) 44 | } 45 | 46 | val R1 = ResourceId(1L, 1L) 47 | val R2 = ResourceId(2L, 2L) 48 | val R3 = ResourceId(3L, 3L) 49 | val R4 = ResourceId(4L, 4L) 50 | 51 | const val TAG1 = "tag1" 52 | const val TAG2 = "tag2" 53 | const val TAG3 = "tag3" 54 | const val TAG4 = "tag4" 55 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_bmp.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/analytics/gallery/GalleryAnalyticsImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.analytics.gallery 2 | 3 | import dev.arkbuilders.navigator.analytics.trackEvent 4 | import dev.arkbuilders.navigator.analytics.trackScreen 5 | import org.matomo.sdk.Tracker 6 | 7 | class GalleryAnalyticsImpl( 8 | private val matomoTracker: Tracker 9 | ) : GalleryAnalytics { 10 | override fun trackScreen() = matomoTracker.trackScreen { 11 | screen(SCREEN_NAME) 12 | } 13 | 14 | override fun trackResOpen() = matomoTracker.trackScreenEvent("Resource open") 15 | 16 | override fun trackResShare() = matomoTracker.trackScreenEvent("Resource share") 17 | 18 | override fun trackResInfo() = matomoTracker.trackScreenEvent("Resource info") 19 | 20 | override fun trackResEdit() = matomoTracker.trackScreenEvent("Resource edit") 21 | 22 | override fun trackResRemove() = matomoTracker.trackScreenEvent("Resource remove") 23 | 24 | override fun trackTagSelect() = matomoTracker.trackScreenEvent("Tag select") 25 | 26 | override fun trackTagRemove() = matomoTracker.trackScreenEvent("Tag remove") 27 | 28 | override fun trackTagsEdit() = matomoTracker.trackScreenEvent("Tags edit") 29 | 30 | private fun Tracker.trackScreenEvent(action: String) = this.trackEvent { 31 | event(SCREEN_NAME, action) 32 | } 33 | 34 | companion object { 35 | private const val SCREEN_NAME = "Gallery screen" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_ts.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_svg.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/popup_gallery_tag_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/di/modules/AppModule.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.di.modules 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dev.arkbuilders.navigator.data.preferences.Preferences 7 | import dev.arkbuilders.navigator.data.preferences.PreferencesImpl 8 | import dev.arkbuilders.navigator.data.utils.DevicePathsExtractor 9 | import dev.arkbuilders.navigator.data.utils.DevicePathsExtractorImpl 10 | import dev.arkbuilders.navigator.presentation.App 11 | import dev.arkbuilders.navigator.presentation.utils.StringProvider 12 | import org.matomo.sdk.Matomo 13 | import org.matomo.sdk.Tracker 14 | import org.matomo.sdk.TrackerBuilder 15 | import javax.inject.Singleton 16 | 17 | @Module 18 | class AppModule { 19 | 20 | @Singleton 21 | @Provides 22 | fun stringProvider(ctx: Context): StringProvider { 23 | return StringProvider(ctx) 24 | } 25 | 26 | @Provides 27 | @Singleton 28 | fun provideUserPreferences(ctx: Context): Preferences = 29 | PreferencesImpl(ctx) 30 | 31 | @Provides 32 | @Singleton 33 | fun provideDevicePathsExtractor(application: App): DevicePathsExtractor = 34 | DevicePathsExtractorImpl(application) 35 | 36 | @Provides 37 | @Singleton 38 | fun provideMatomoAnalytics(ctx: Context): Tracker = 39 | TrackerBuilder.createDefault( 40 | "https://ark-builders.matomo.cloud/matomo.php", 41 | 2 42 | ).build(Matomo.getInstance(ctx)) 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_mpg.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_mp3.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_view_folder_tree_device.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 27 | 28 | 32 | 33 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_3gp.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/stats/category/StatsCategoryStorage.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.stats.category 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.flow.MutableSharedFlow 5 | import kotlinx.coroutines.flow.debounce 6 | import kotlinx.coroutines.flow.launchIn 7 | import kotlinx.coroutines.flow.onEach 8 | import kotlinx.coroutines.launch 9 | import dev.arkbuilders.arklib.arkFolder 10 | import dev.arkbuilders.arklib.arkStats 11 | import dev.arkbuilders.arklib.data.stats.StatsEvent 12 | import timber.log.Timber 13 | import java.nio.file.Path 14 | 15 | private const val FLUSH_INTERVAL = 10_000L 16 | 17 | abstract class StatsCategoryStorage( 18 | val root: Path, 19 | private val scope: CoroutineScope 20 | ) { 21 | abstract val fileName: String 22 | private val flushFlow = MutableSharedFlow().also { flow -> 23 | flow.debounce(FLUSH_INTERVAL).onEach { 24 | // There may be an exception after root is removed 25 | try { 26 | flush() 27 | } catch (e: Exception) { 28 | Timber.e(e) 29 | } 30 | }.launchIn(scope) 31 | } 32 | 33 | abstract suspend fun init() 34 | 35 | abstract fun handleEvent(event: StatsEvent) 36 | abstract fun provideData(): T 37 | protected abstract fun flush() 38 | 39 | fun locateStorage(): Path? = root.arkFolder().arkStats().resolve(fileName) 40 | 41 | protected fun requestFlush() = scope.launch { 42 | flushFlow.emit(Unit) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/utils/FullscreenHelper.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.utils 2 | 3 | import android.os.Build 4 | import android.view.Window 5 | import android.view.WindowManager 6 | import androidx.core.view.ViewCompat 7 | import androidx.core.view.WindowInsetsCompat 8 | import androidx.core.view.WindowInsetsControllerCompat 9 | 10 | object FullscreenHelper { 11 | 12 | fun setStatusBarVisibility(isVisible: Boolean, window: Window) = 13 | if (isVisible) showStatusBar(window) else hideStatusBar(window) 14 | 15 | private fun hideStatusBar(window: Window) { 16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 17 | val windowInsetsController = 18 | ViewCompat.getWindowInsetsController(window.decorView) ?: return 19 | windowInsetsController.systemBarsBehavior = 20 | WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 21 | windowInsetsController.hide(WindowInsetsCompat.Type.statusBars()) 22 | } else { 23 | window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 24 | } 25 | } 26 | 27 | private fun showStatusBar(window: Window) { 28 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 29 | val windowInsetsController = window.insetsController ?: return 30 | windowInsetsController.show(WindowInsetsCompat.Type.statusBars()) 31 | } else { 32 | window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_wma.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 29 | 30 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_xlsx.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_docx.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/view/LoadingTextView.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.view 2 | 3 | import android.content.Context 4 | import android.os.Handler 5 | import android.os.Looper 6 | import android.util.AttributeSet 7 | import androidx.appcompat.widget.AppCompatTextView 8 | import androidx.core.view.isVisible 9 | 10 | class LoadingTextView(context: Context, attrs: AttributeSet?) : 11 | AppCompatTextView(context, attrs) { 12 | 13 | private var isLoading = false 14 | private var isMakingDots = false 15 | 16 | var loadingText: String = "" 17 | set(value) { 18 | field = value 19 | text = loadingText 20 | if (text.isNotEmpty() && !isMakingDots) { 21 | makeLoadingDots() 22 | } 23 | } 24 | 25 | private var dotCount = 0 26 | set(value) { 27 | field = if (value >= 4) { 28 | 0 29 | } else { 30 | value 31 | } 32 | } 33 | 34 | fun setVisibilityAndLoadingStatus(visibility: Int) { 35 | this.visibility = visibility 36 | dotCount = 0 37 | isLoading = isVisible 38 | } 39 | 40 | private fun makeLoadingDots() { 41 | isMakingDots = true 42 | Handler(Looper.getMainLooper()).postDelayed({ 43 | if (isLoading) { 44 | dotCount++ 45 | 46 | val textToDisplay = "$loadingText${".".repeat(dotCount)}" 47 | this.text = textToDisplay 48 | makeLoadingDots() 49 | } else { 50 | isMakingDots = false 51 | } 52 | }, 500) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/analytics/AnalyticsModule.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.analytics 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dev.arkbuilders.navigator.analytics.folders.FoldersAnalytics 7 | import dev.arkbuilders.navigator.analytics.folders.FoldersAnalyticsImpl 8 | import dev.arkbuilders.navigator.analytics.gallery.GalleryAnalytics 9 | import dev.arkbuilders.navigator.analytics.gallery.GalleryAnalyticsImpl 10 | import dev.arkbuilders.navigator.analytics.resources.ResourcesAnalytics 11 | import dev.arkbuilders.navigator.analytics.resources.ResourcesAnalyticsImpl 12 | import dev.arkbuilders.navigator.analytics.settings.SettingsAnalytics 13 | import dev.arkbuilders.navigator.analytics.settings.SettingsAnalyticsImpl 14 | import org.matomo.sdk.Tracker 15 | import javax.inject.Singleton 16 | 17 | @Module 18 | class AnalyticsModule { 19 | 20 | @Singleton 21 | @Provides 22 | fun provideFolderAnalytics( 23 | matomoTracker: Tracker, 24 | context: Context 25 | ): FoldersAnalytics = 26 | FoldersAnalyticsImpl(matomoTracker = matomoTracker, context = context) 27 | 28 | @Singleton 29 | @Provides 30 | fun provideResourcesAnalytics( 31 | matomoTracker: Tracker 32 | ): ResourcesAnalytics = ResourcesAnalyticsImpl(matomoTracker) 33 | 34 | @Singleton 35 | @Provides 36 | fun provideGalleryAnalytics( 37 | matomoTracker: Tracker 38 | ): GalleryAnalytics = GalleryAnalyticsImpl(matomoTracker) 39 | 40 | @Singleton 41 | @Provides 42 | fun provideSettingsAnalytics( 43 | matomoTracker: Tracker 44 | ): SettingsAnalytics = SettingsAnalyticsImpl(matomoTracker) 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_webm.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/view/UserSwitchMaterial.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.CompoundButton 6 | import com.google.android.material.switchmaterial.SwitchMaterial 7 | import dev.arkbuilders.navigator.data.utils.LogTags.SETTINGS_SCREEN 8 | import timber.log.Timber 9 | 10 | class UserSwitchMaterial( 11 | context: Context, 12 | attrs: AttributeSet 13 | ) : SwitchMaterial(context, attrs) { 14 | 15 | private var checkedChangeListener: CustomCheckedChangeListener? = null 16 | 17 | fun setOnUserCheckedChangeListener( 18 | callback: (isChecked: Boolean) -> Unit 19 | ) { 20 | Timber.d( 21 | SETTINGS_SCREEN, 22 | "setOnUserCheckedChangeListener: ${this.id}, " + "$isChecked" 23 | ) 24 | if (checkedChangeListener == null) { 25 | checkedChangeListener = CustomCheckedChangeListener(callback) 26 | } else { 27 | checkedChangeListener?.callback = callback 28 | } 29 | 30 | this.setOnCheckedChangeListener(checkedChangeListener) 31 | } 32 | 33 | fun toggleSwitchSilent(mIsChecked: Boolean) { 34 | if (isChecked != mIsChecked) { 35 | isChecked = mIsChecked 36 | jumpDrawablesToCurrentState() 37 | } 38 | } 39 | 40 | private class CustomCheckedChangeListener( 41 | var callback: (isChecked: Boolean) -> Unit 42 | ) : OnCheckedChangeListener { 43 | override fun onCheckedChanged(button: CompoundButton, isChecked: Boolean) { 44 | if (button.isPressed) { 45 | callback(isChecked) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/stats/StatsStorageRepo.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.stats 2 | 3 | import kotlinx.coroutines.flow.SharedFlow 4 | import dev.arkbuilders.arklib.data.index.ResourceIndex 5 | import dev.arkbuilders.arklib.data.index.RootIndex 6 | import dev.arkbuilders.arklib.data.stats.StatsEvent 7 | import dev.arkbuilders.arklib.user.tags.RootTagsStorage 8 | import dev.arkbuilders.arklib.user.tags.TagsStorageRepo 9 | import dev.arkbuilders.navigator.data.preferences.Preferences 10 | import java.nio.file.Path 11 | 12 | class StatsStorageRepo( 13 | private val tagsStorageRepo: TagsStorageRepo, 14 | private val preferences: Preferences, 15 | private val statsFlow: SharedFlow 16 | ) { 17 | private val storageByRoot = mutableMapOf() 18 | 19 | suspend fun provide(index: ResourceIndex): StatsStorage { 20 | val roots = index.roots 21 | 22 | return if (roots.size > 1) { 23 | val shards = roots.map { 24 | val tagsStorage = tagsStorageRepo.provide(it) 25 | provide(it, tagsStorage) 26 | } 27 | 28 | AggregatedStatsStorage(shards) 29 | } else { 30 | val root = roots.iterator().next() 31 | val tagsStorage = tagsStorageRepo.provide(root) 32 | provide(root, tagsStorage) 33 | } 34 | } 35 | 36 | private suspend fun provide( 37 | root: RootIndex, 38 | tagsStorage: RootTagsStorage 39 | ): PlainStatsStorage = 40 | storageByRoot[root.path] ?: PlainStatsStorage( 41 | root, 42 | preferences, 43 | tagsStorage, 44 | statsFlow 45 | ).also { 46 | it.init() 47 | storageByRoot[root.path] = it 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/utils/PathExt.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.utils 2 | 3 | import java.nio.file.Path 4 | import java.nio.file.Paths 5 | import kotlin.io.path.exists 6 | import kotlin.io.path.extension 7 | import kotlin.io.path.nameWithoutExtension 8 | import kotlin.io.path.notExists 9 | 10 | val ROOT_PATH: Path = Paths.get("/") 11 | 12 | val ANDROID_DIRECTORY: Path = Paths.get("Android") 13 | 14 | fun Path.findNotExistCopyName(name: Path): Path { 15 | val originalNamePath = this.resolve(name.fileName) 16 | if (originalNamePath.notExists()) { 17 | return originalNamePath 18 | } 19 | 20 | var filesCounter = 1 21 | 22 | val formatNameWithCounter = 23 | "${name.nameWithoutExtension}_$filesCounter.${name.extension}" 24 | 25 | var newPath = this.resolve(formatNameWithCounter) 26 | 27 | while (newPath.exists()) { 28 | newPath = this.resolve(formatNameWithCounter) 29 | filesCounter++ 30 | } 31 | return newPath 32 | } 33 | 34 | fun findLongestCommonPrefix(paths: List): Path { 35 | if (paths.isEmpty()) { 36 | throw IllegalArgumentException( 37 | "Can't search for common prefix among empty collection" 38 | ) 39 | } 40 | 41 | if (paths.size == 1) { 42 | return paths.first() 43 | } 44 | 45 | return tailrec(ROOT_PATH, paths).first 46 | } 47 | 48 | private fun tailrec(prefix: Path, paths: List): Pair> { 49 | val grouped = paths.groupBy { it.getName(0) } 50 | if (grouped.size > 1) { 51 | return prefix to paths 52 | } 53 | 54 | val resolvedPrefix = prefix.resolve(grouped.keys.first()) 55 | val shortened = grouped.values.first() 56 | .map { resolvedPrefix.relativize(it) } 57 | 58 | return tailrec(resolvedPrefix, shortened) 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dice.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 16 | 17 | 28 | 29 | 36 | 37 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/screen/resources/ResourcesView.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.screen.resources 2 | 3 | import dev.arkbuilders.components.tagselector.QueryMode 4 | import dev.arkbuilders.navigator.presentation.common.CommonMvpView 5 | import moxy.viewstate.strategy.AddToEndSingleStrategy 6 | import moxy.viewstate.strategy.SkipStrategy 7 | import moxy.viewstate.strategy.StateStrategyType 8 | import java.nio.file.Path 9 | 10 | @StateStrategyType(AddToEndSingleStrategy::class) 11 | interface ResourcesView : CommonMvpView { 12 | fun init(ascending: Boolean, sortByScoresEnabled: Boolean) 13 | fun initResourcesAdapter() 14 | fun updateResourcesAdapter() 15 | fun setProgressVisibility(isVisible: Boolean, withText: String = "") 16 | fun setToolbarTitle(title: String) 17 | fun updateMenu(queryMode: QueryMode) 18 | fun updateOrderBtn(isAscending: Boolean) 19 | fun setSelectingEnabled(enabled: Boolean) 20 | fun setSelectingCount(selected: Int, all: Int) 21 | fun setPreviewGenerationProgress(isVisible: Boolean) 22 | fun setMetadataExtractionProgress(isVisible: Boolean) 23 | 24 | @StateStrategyType(SkipStrategy::class) 25 | fun toastResourcesSelected(selected: Int) 26 | 27 | @StateStrategyType(SkipStrategy::class) 28 | fun toastResourcesSelectedFocusMode(selected: Int, hidden: Int) 29 | 30 | @StateStrategyType(SkipStrategy::class) 31 | fun toastPathsFailed(failedPaths: List) 32 | 33 | @StateStrategyType(SkipStrategy::class) 34 | fun onSelectingChanged(enabled: Boolean) 35 | 36 | @StateStrategyType(SkipStrategy::class) 37 | fun clearStackedToasts() 38 | 39 | @StateStrategyType(SkipStrategy::class) 40 | fun shareResources(resources: List) 41 | 42 | @StateStrategyType(SkipStrategy::class) 43 | fun displayStorageException(label: String, msg: String) 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/test/java/dev/arkbuilders/navigator/data/utils/DevicePathsExtractorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.utils 2 | 3 | import dev.arkbuilders.navigator.presentation.App 4 | import io.mockk.every 5 | import io.mockk.mockk 6 | import io.mockk.verify 7 | import org.junit.jupiter.api.Assertions.assertEquals 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | import org.junit.runner.RunWith 11 | import org.mockito.junit.MockitoJUnitRunner 12 | import java.io.File 13 | import java.nio.file.Path 14 | import java.nio.file.Paths 15 | import kotlin.io.path.name 16 | 17 | @RunWith(MockitoJUnitRunner::class) 18 | class DevicePathsExtractorTest { 19 | 20 | private val mockedApplication = mockk() 21 | 22 | private lateinit var testee: DevicePathsExtractor 23 | 24 | @BeforeEach 25 | fun setUp() { 26 | testee = DevicePathsExtractorImpl( 27 | appInstance = mockedApplication 28 | ) 29 | } 30 | 31 | @Test 32 | fun givenApplicationAvailable_whenGetExternalFileDirs_thenReturnCorrectResult() { 33 | val mockedFile = mockk() 34 | val files = listOf(mockedFile) 35 | every { mockedApplication.getExternalFilesDirs(null) } returns files.toTypedArray() 36 | every { mockedFile.exists() } returns true 37 | 38 | val mockedPath = mockk() 39 | every { mockedFile.toPath() } returns mockedPath 40 | every { mockedPath.toRealPath() } returns mockedPath 41 | 42 | val directoryIterator: MutableIterator = arrayListOf( 43 | Paths.get("PATH") 44 | ).iterator() 45 | every { mockedPath.iterator() } returns directoryIterator 46 | 47 | val result = testee.listDevices() 48 | 49 | verify { mockedApplication.getExternalFilesDirs(null) } 50 | assertEquals(1, result.size) 51 | assertEquals("PATH", result[0].name) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/utils/extra/ExtraLoader.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.utils.extra 2 | 3 | import android.widget.TextView 4 | import dev.arkbuilders.arklib.data.meta.Metadata 5 | import dev.arkbuilders.navigator.presentation.utils.makeGone 6 | 7 | object ExtraLoader { 8 | fun load(meta: Metadata, extraTVs: List, verbose: Boolean) { 9 | extraTVs.forEach { it.makeGone() } 10 | 11 | when (meta) { 12 | is Metadata.Video -> VideoExtraLoader.load( 13 | meta, 14 | extraTVs[0], 15 | extraTVs[1] 16 | ) 17 | is Metadata.Document -> DocumentExtraLoader.load( 18 | meta, 19 | extraTVs[0], 20 | verbose 21 | ) 22 | is Metadata.Link -> LinkExtraLoader.load( 23 | meta, 24 | extraTVs[1], 25 | verbose 26 | ) 27 | else -> {} 28 | } 29 | } 30 | 31 | fun loadWithLabel( 32 | meta: Metadata, 33 | kindPlaceholders: List 34 | ) { 35 | require(kindPlaceholders.size == 3) 36 | 37 | kindPlaceholders.forEach { it.makeGone() } 38 | 39 | when (meta) { 40 | is Metadata.Video -> VideoExtraLoader.loadInfo( 41 | meta, 42 | kindPlaceholders[0], 43 | kindPlaceholders[1] 44 | ) 45 | is Metadata.Document -> DocumentExtraLoader.loadWithLabel( 46 | meta, 47 | kindPlaceholders[0] 48 | ) 49 | is Metadata.Link -> LinkExtraLoader.loadWithLabel( 50 | meta, 51 | kindPlaceholders[0], 52 | kindPlaceholders[1], 53 | kindPlaceholders[2] 54 | ) 55 | else -> {} 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/res/xml/edit_tags_motion_scene.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 31 | 32 | 33 | 34 | 40 | 41 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/popup_selected_resources_actions.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 17 | 18 | 22 | 23 | 27 | 28 | 32 | 33 | 37 | 38 | 42 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/dialog/ExplainPermsDialog.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.dialog 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.graphics.drawable.ColorDrawable 6 | import android.os.Bundle 7 | import android.view.View 8 | import androidx.fragment.app.DialogFragment 9 | import by.kirich1409.viewbindingdelegate.viewBinding 10 | import com.airbnb.lottie.LottieCompositionFactory 11 | import dev.arkbuilders.navigator.R 12 | import dev.arkbuilders.navigator.databinding.DialogExplainPermsBinding 13 | import dev.arkbuilders.navigator.presentation.App 14 | import dev.arkbuilders.navigator.data.PermissionsHelper 15 | import javax.inject.Inject 16 | 17 | class ExplainPermsDialog : DialogFragment(R.layout.dialog_explain_perms) { 18 | private val viewBinding by viewBinding(DialogExplainPermsBinding::bind) 19 | 20 | @Inject 21 | lateinit var permsHelper: PermissionsHelper 22 | 23 | override fun onAttach(context: Context) { 24 | App.instance.appComponent.inject(this) 25 | super.onAttach(context) 26 | } 27 | 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | dialog?.apply { 31 | window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 32 | setCancelable(false) 33 | setCanceledOnTouchOutside(false) 34 | } 35 | viewBinding.btnAllow.setOnClickListener { 36 | permsHelper.askForWritePermissions(this@ExplainPermsDialog) 37 | dismiss() 38 | } 39 | viewBinding.btnExit.setOnClickListener { 40 | activity?.finish() 41 | } 42 | } 43 | 44 | companion object { 45 | fun newInstance(context: Context): ExplainPermsDialog { 46 | LottieCompositionFactory.fromRawResSync(context, R.raw.anim_file_access) 47 | return ExplainPermsDialog() 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/stats/category/TagQueriedTSStorage.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.stats.category 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.encodeToString 6 | import kotlinx.serialization.json.Json 7 | import kotlinx.serialization.json.decodeFromStream 8 | import dev.arkbuilders.arklib.data.stats.StatsEvent 9 | import dev.arkbuilders.arklib.user.tags.Tag 10 | import timber.log.Timber 11 | import java.nio.file.Path 12 | import kotlin.io.path.inputStream 13 | import kotlin.io.path.notExists 14 | import kotlin.io.path.writeText 15 | 16 | class TagQueriedTSStorage( 17 | root: Path, 18 | scope: CoroutineScope 19 | ) : StatsCategoryStorage>(root, scope) { 20 | override val fileName = "tag-queried-ts" 21 | private val tagQueriedTS = mutableMapOf() 22 | 23 | override suspend fun init() { 24 | val storage = locateStorage() 25 | if (storage?.notExists() == true) return 26 | val json = storage?.let { 27 | Json.decodeFromStream(it.inputStream()) 28 | } 29 | json?.let { tagQueriedTS.putAll(it.data) } 30 | Timber.i("initialized with $tagQueriedTS") 31 | } 32 | 33 | override fun handleEvent(event: StatsEvent) { 34 | when (event) { 35 | is StatsEvent.PlainTagUsed -> 36 | tagQueriedTS[event.tag] = System.currentTimeMillis() 37 | is StatsEvent.KindTagUsed -> 38 | tagQueriedTS[event.kind.name] = System.currentTimeMillis() 39 | else -> return 40 | } 41 | requestFlush() 42 | } 43 | 44 | override fun provideData() = tagQueriedTS 45 | 46 | override fun flush() { 47 | val data = Json.encodeToString(JsonTagQueriedTS(tagQueriedTS)) 48 | locateStorage()?.writeText(data) 49 | Timber.i("flushed with $tagQueriedTS") 50 | } 51 | } 52 | 53 | @Serializable 54 | private class JsonTagQueriedTS(val data: Map) 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_view_folder_tree_favorite.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 22 | 23 | 29 | 30 | 34 | 35 | 41 | 42 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/dialog/InfoDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.dialog 2 | 3 | import android.graphics.Color 4 | import android.graphics.drawable.ColorDrawable 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.core.os.bundleOf 10 | import androidx.fragment.app.DialogFragment 11 | import dev.arkbuilders.navigator.databinding.DialogInfoBinding 12 | 13 | class InfoDialogFragment : DialogFragment() { 14 | 15 | private var titleText: String = "" 16 | private var descriptionText: String = "" 17 | 18 | private lateinit var binding: DialogInfoBinding 19 | 20 | override fun onCreateView( 21 | inflater: LayoutInflater, 22 | container: ViewGroup?, 23 | savedInstanceState: Bundle? 24 | ): View { 25 | super.onCreateView(inflater, container, savedInstanceState) 26 | 27 | binding = DialogInfoBinding.inflate(inflater, container, false) 28 | 29 | dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 30 | 31 | titleText = requireArguments()[TITLE_TAG] as String 32 | descriptionText = requireArguments()[DESCRIPTION_TAG] as String 33 | 34 | binding.titleTV.text = titleText 35 | binding.infoTV.text = descriptionText 36 | 37 | binding.posBtn.setOnClickListener { 38 | dismiss() 39 | } 40 | 41 | return binding.root 42 | } 43 | 44 | companion object { 45 | const val TITLE_TAG = "title" 46 | const val DESCRIPTION_TAG = "description" 47 | const val BASE_INFO_DIALOG_TAG = "baseInfoDialogFragment" 48 | 49 | fun newInstance( 50 | title: String, 51 | description: String 52 | ): InfoDialogFragment = InfoDialogFragment().also { f -> 53 | f.arguments = bundleOf( 54 | TITLE_TAG to title, 55 | DESCRIPTION_TAG to description 56 | ) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/preferences/Preferences.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.preferences 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | interface Preferences { 6 | suspend fun get(key: PreferenceKey): T 7 | suspend fun set(key: PreferenceKey, value: T) 8 | suspend fun flow(key: PreferenceKey): Flow 9 | 10 | suspend fun clearPreferences() { 11 | val preferencesToReset = listOf( 12 | PreferenceKey.Sorting, 13 | PreferenceKey.IsSortingAscending, 14 | PreferenceKey.CrashReport, 15 | PreferenceKey.ImgCacheReplication, 16 | PreferenceKey.IndexReplication, 17 | PreferenceKey.RemovingLostResourcesTags, 18 | PreferenceKey.BackupEnabled, 19 | PreferenceKey.ShortFileNames, 20 | PreferenceKey.SortByScores 21 | ) 22 | 23 | preferencesToReset.forEach { 24 | set(it, it.defaultValue) 25 | } 26 | } 27 | } 28 | 29 | sealed class PreferenceKey(val defaultValue: T) { 30 | object Sorting : PreferenceKey(0) 31 | object IsSortingAscending : PreferenceKey(true) 32 | object TagsSortingSelector : PreferenceKey(0) 33 | object TagsSortingSelectorAsc : PreferenceKey(false) 34 | object TagsSortingEdit : PreferenceKey(0) 35 | object TagsSortingEditAsc : PreferenceKey(false) 36 | object CrashReport : PreferenceKey(true) 37 | object ImgCacheReplication : PreferenceKey(false) 38 | object IndexReplication : PreferenceKey(false) 39 | object RemovingLostResourcesTags : PreferenceKey(false) 40 | object ShowKinds : PreferenceKey(false) 41 | object WasRootsScanShown : PreferenceKey(false) 42 | object BackupEnabled : PreferenceKey(true) 43 | object ShortFileNames : PreferenceKey(true) 44 | object CollectTagUsageStats : PreferenceKey(true) 45 | object SortByScores : PreferenceKey(false) 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/stats/category/TagLabeledTSStorage.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.stats.category 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.encodeToString 6 | import kotlinx.serialization.json.Json 7 | import kotlinx.serialization.json.decodeFromStream 8 | import dev.arkbuilders.arklib.data.stats.StatsEvent 9 | import dev.arkbuilders.arklib.user.tags.Tag 10 | import timber.log.Timber 11 | import java.nio.file.Path 12 | import kotlin.io.path.inputStream 13 | import kotlin.io.path.notExists 14 | import kotlin.io.path.writeText 15 | 16 | class TagLabeledTSStorage( 17 | root: Path, 18 | scope: CoroutineScope 19 | ) : StatsCategoryStorage>(root, scope) { 20 | override val fileName: String = "tag-labeled-ts" 21 | private val tagLabeledTS = mutableMapOf() 22 | 23 | override suspend fun init() { 24 | val storage = locateStorage()?.also { if (it.notExists()) return } 25 | storage!!.inputStream().use { 26 | val json = Json.decodeFromStream(it) 27 | tagLabeledTS.putAll(json.data) 28 | Timber.i("initialized with $tagLabeledTS") 29 | } 30 | } 31 | 32 | override fun handleEvent(event: StatsEvent) { 33 | when (event) { 34 | is StatsEvent.TagsChanged -> with(event) { 35 | val same = oldTags.intersect(newTags) 36 | val new = newTags.minus(same) 37 | new.forEach { 38 | tagLabeledTS[it] = System.currentTimeMillis() 39 | } 40 | } 41 | else -> return 42 | } 43 | requestFlush() 44 | } 45 | 46 | override fun provideData() = tagLabeledTS 47 | 48 | override fun flush() { 49 | val data = Json.encodeToString(JsonTagLabeledTS(tagLabeledTS)) 50 | locateStorage()?.writeText(data) 51 | Timber.i("flushed with $tagLabeledTS") 52 | } 53 | } 54 | 55 | @Serializable 56 | private class JsonTagLabeledTS(val data: Map) 57 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/stats/AggregatedStatsStorage.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.data.stats 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.joinAll 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.withContext 7 | import dev.arkbuilders.arklib.data.stats.StatsEvent 8 | import dev.arkbuilders.arklib.user.tags.Tag 9 | 10 | class AggregatedStatsStorage(private val shards: List) : StatsStorage { 11 | 12 | override suspend fun init() = withContext(Dispatchers.IO) { 13 | shards.map { launch { it.init() } }.joinAll() 14 | } 15 | 16 | override fun handleEvent(event: StatsEvent) = 17 | shards.forEach { it.handleEvent(event) } 18 | 19 | override fun statsTagLabeledAmount(): Map = 20 | shards 21 | .map { it.statsTagLabeledAmount() } 22 | .fold(mutableMapOf()) { acc, shard -> 23 | shard.forEach { (tag, amount) -> 24 | acc.merge(tag, amount, Int::plus) 25 | } 26 | acc 27 | } 28 | 29 | override fun statsTagQueriedAmount(): Map = 30 | shards 31 | .map { it.statsTagQueriedAmount() } 32 | .fold(mutableMapOf()) { acc, shard -> 33 | shard.forEach { (tag, amount) -> 34 | acc.merge(tag, amount, Int::plus) 35 | } 36 | acc 37 | } 38 | 39 | override fun statsTagQueriedTS(): Map = shards 40 | .map { it.statsTagQueriedTS() } 41 | .fold(mutableMapOf()) { acc, shard -> 42 | shard.forEach { (tag, shardTS) -> 43 | acc[tag] = maxOf(acc[tag] ?: -1, shardTS) 44 | } 45 | acc 46 | } 47 | 48 | override fun statsTagLabeledTS(): Map = shards 49 | .map { it.statsTagLabeledTS() } 50 | .fold(mutableMapOf()) { acc, shard -> 51 | shard.forEach { (tag, shardTS) -> 52 | acc[tag] = maxOf(acc[tag] ?: -1, shardTS) 53 | } 54 | acc 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/data/stats/category/TagQueriedNStorage.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class) 2 | 3 | package dev.arkbuilders.navigator.data.stats.category 4 | 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.serialization.ExperimentalSerializationApi 7 | import kotlinx.serialization.Serializable 8 | import kotlinx.serialization.encodeToString 9 | import kotlinx.serialization.json.Json 10 | import kotlinx.serialization.json.decodeFromStream 11 | import dev.arkbuilders.arklib.data.stats.StatsEvent 12 | import dev.arkbuilders.arklib.user.tags.Tag 13 | import timber.log.Timber 14 | import java.nio.file.Path 15 | import kotlin.io.path.inputStream 16 | import kotlin.io.path.notExists 17 | import kotlin.io.path.writeText 18 | 19 | class TagQueriedNStorage( 20 | root: Path, 21 | scope: CoroutineScope 22 | ) : StatsCategoryStorage>(root, scope) { 23 | override val fileName: String = "tag-queried-n" 24 | 25 | private val tagQueriedN = mutableMapOf() 26 | 27 | override suspend fun init() { 28 | val storage = locateStorage()?.also { if (it.notExists()) return } 29 | storage!!.inputStream().use { 30 | val json = Json.decodeFromStream(it) 31 | tagQueriedN.putAll(json.data) 32 | Timber.i("initialized with $tagQueriedN") 33 | } 34 | } 35 | 36 | override fun handleEvent(event: StatsEvent) { 37 | when (event) { 38 | is StatsEvent.PlainTagUsed -> 39 | tagQueriedN.merge(event.tag, 1, Int::plus) 40 | is StatsEvent.KindTagUsed -> 41 | tagQueriedN.merge(event.kind.name, 1, Int::plus) 42 | else -> return 43 | } 44 | requestFlush() 45 | } 46 | 47 | override fun provideData() = tagQueriedN 48 | 49 | override fun flush() { 50 | val data = Json.encodeToString(JsonTagQueriedN(tagQueriedN)) 51 | locateStorage()?.writeText(data) 52 | Timber.i("flushed with $tagQueriedN") 53 | } 54 | } 55 | 56 | @Serializable 57 | private class JsonTagQueriedN(val data: Map) 58 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_boolean_preference.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 | 34 | 35 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/java/dev/arkbuilders/navigator/presentation/screen/resources/adapter/ResourcesRVAdapter.kt: -------------------------------------------------------------------------------- 1 | package dev.arkbuilders.navigator.presentation.screen.resources.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import dev.arkbuilders.navigator.databinding.ItemFileGridBinding 7 | import dev.arkbuilders.navigator.presentation.App 8 | 9 | class ResourcesRVAdapter( 10 | private val presenter: ResourcesGridPresenter 11 | ) : RecyclerView.Adapter() { 12 | private var viewHolders = mutableListOf() 13 | 14 | override fun getItemCount() = presenter.getCount() 15 | 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 17 | FileItemViewHolder( 18 | ItemFileGridBinding.inflate( 19 | LayoutInflater.from(parent.context), 20 | parent, 21 | false 22 | ) 23 | ).also { 24 | App.instance.appComponent.inject(it) 25 | } 26 | 27 | override fun onBindViewHolder(holder: FileItemViewHolder, position: Int) { 28 | presenter.bindView(holder) 29 | 30 | holder.binding.root.setOnClickListener { 31 | if (presenter.selectingEnabled) { 32 | presenter.onItemSelectChanged(holder) 33 | } else { 34 | presenter.onItemClick(position) 35 | } 36 | } 37 | holder.binding.root.setOnLongClickListener { 38 | if (presenter.selectingEnabled) { 39 | presenter.onSelectedItemLongClick(holder) 40 | } else { 41 | presenter.onSelectingChanged(true) 42 | holder.binding.root.performClick() 43 | } 44 | 45 | return@setOnLongClickListener true 46 | } 47 | viewHolders.add(holder) 48 | } 49 | 50 | override fun onViewRecycled(holder: FileItemViewHolder) { 51 | super.onViewRecycled(holder) 52 | viewHolders.remove(holder) 53 | } 54 | 55 | fun onSelectingChanged(enabled: Boolean) { 56 | viewHolders.forEach { 57 | it.onSelectingChanged(enabled) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 18 | 19 | 23 | 24 | 25 |