├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── enhancement-feature-request.md └── workflows │ ├── nightly.yml │ └── stable.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── compose_compiler_config.conf ├── lint-baseline.xml ├── proguard-rules.pro ├── schemas │ ├── com.dot.gallery.feature_node.data.data_source.InternalDatabase │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ ├── 4.json │ │ ├── 5.json │ │ ├── 6.json │ │ ├── 7.json │ │ └── 8.json │ ├── debug │ │ └── com.dot.gallery.feature_node.data.data_source.InternalDatabase │ │ │ └── 1.json │ ├── release │ │ └── com.dot.gallery.feature_node.data.data_source.InternalDatabase │ │ │ └── 1.json │ └── staging │ │ └── com.dot.gallery.feature_node.data.data_source.InternalDatabase │ │ └── 1.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dot │ │ └── gallery │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── mobile_ica_8bit_with_metadata.tflite │ ├── ic_launcher-playstore.png │ ├── kotlin │ │ └── com │ │ │ └── dot │ │ │ └── gallery │ │ │ ├── GalleryApp.kt │ │ │ ├── core │ │ │ ├── Constants.kt │ │ │ ├── DatabaseUpdaterWorker.kt │ │ │ ├── MetadataCollectionWorker.kt │ │ │ ├── Resource.kt │ │ │ ├── Settings.kt │ │ │ ├── decoder │ │ │ │ ├── DecoderExt.kt │ │ │ │ ├── EncryptedBitmapFactoryDecoder.kt │ │ │ │ ├── EncryptedDecoderExt.kt │ │ │ │ ├── EncryptedRegionDecoder.kt │ │ │ │ ├── EncryptedRegionDecoderExt.kt │ │ │ │ ├── EncryptedVideoFrameDecoder.kt │ │ │ │ ├── SketchHeifDecoder.kt │ │ │ │ └── SketchJxlDecoder.kt │ │ │ ├── presentation │ │ │ │ └── components │ │ │ │ │ ├── AppBar.kt │ │ │ │ │ ├── CheckBox.kt │ │ │ │ │ ├── DragHandle.kt │ │ │ │ │ ├── EmptyAlbum.kt │ │ │ │ │ ├── EmptyMedia.kt │ │ │ │ │ ├── ErrorComponent.kt │ │ │ │ │ ├── FilterComponent.kt │ │ │ │ │ ├── LoadingAlbum.kt │ │ │ │ │ ├── LoadingMedia.kt │ │ │ │ │ ├── MediaItemHeader.kt │ │ │ │ │ ├── ModalSheet.kt │ │ │ │ │ ├── NavigationActions.kt │ │ │ │ │ ├── NavigationBarSpacer.kt │ │ │ │ │ ├── NavigationButton.kt │ │ │ │ │ ├── NavigationComp.kt │ │ │ │ │ ├── SelectionSheet.kt │ │ │ │ │ ├── SetupWizard.kt │ │ │ │ │ └── util │ │ │ │ │ ├── AutoResizeText.kt │ │ │ │ │ ├── BatteryExt.kt │ │ │ │ │ ├── OnLifecycleEvent.kt │ │ │ │ │ ├── PermissionsExt.kt │ │ │ │ │ ├── ShadowModifier.kt │ │ │ │ │ ├── StickyHeaderGrid.kt │ │ │ │ │ └── swipe.kt │ │ │ └── util │ │ │ │ ├── Column.kt │ │ │ │ ├── MediaStoreBuckets.kt │ │ │ │ ├── PickerUtils.kt │ │ │ │ ├── SettingsExt.kt │ │ │ │ └── ext │ │ │ │ ├── ContentResolverExt.kt │ │ │ │ ├── CursorExt.kt │ │ │ │ ├── ExifInterfaceExt.kt │ │ │ │ └── FlowExt.kt │ │ │ ├── feature_node │ │ │ ├── data │ │ │ │ ├── data_source │ │ │ │ │ ├── BlacklistDao.kt │ │ │ │ │ ├── ClassifierDao.kt │ │ │ │ │ ├── InternalDatabase.kt │ │ │ │ │ ├── KeychainHolder.kt │ │ │ │ │ ├── MediaDao.kt │ │ │ │ │ ├── MetadataDao.kt │ │ │ │ │ ├── PinnedDao.kt │ │ │ │ │ ├── VaultDao.kt │ │ │ │ │ └── mediastore │ │ │ │ │ │ ├── MediaQuery.kt │ │ │ │ │ │ └── queries │ │ │ │ │ │ ├── AlbumsFlow.kt │ │ │ │ │ │ ├── MediaFlow.kt │ │ │ │ │ │ ├── MediaUriFlow.kt │ │ │ │ │ │ └── QueryFlow.kt │ │ │ │ └── repository │ │ │ │ │ └── MediaRepositoryImpl.kt │ │ │ ├── domain │ │ │ │ ├── model │ │ │ │ │ ├── Album.kt │ │ │ │ │ ├── AlbumState.kt │ │ │ │ │ ├── ExifAttributes.kt │ │ │ │ │ ├── IgnoredAlbum.kt │ │ │ │ │ ├── InfoRow.kt │ │ │ │ │ ├── LibraryIndicatorState.kt │ │ │ │ │ ├── LocationData.kt │ │ │ │ │ ├── Media.kt │ │ │ │ │ ├── MediaDateCaption.kt │ │ │ │ │ ├── MediaItem.kt │ │ │ │ │ ├── MediaMetadata.kt │ │ │ │ │ ├── MediaMetadataState.kt │ │ │ │ │ ├── MediaState.kt │ │ │ │ │ ├── MediaType.kt │ │ │ │ │ ├── MediaVersion.kt │ │ │ │ │ ├── PinnedAlbum.kt │ │ │ │ │ ├── PlaybackSpeed.kt │ │ │ │ │ ├── TimelineSettings.kt │ │ │ │ │ ├── Vault.kt │ │ │ │ │ ├── VaultState.kt │ │ │ │ │ └── editor │ │ │ │ │ │ ├── Adjustment.kt │ │ │ │ │ │ ├── CropState.kt │ │ │ │ │ │ ├── CropperAction.kt │ │ │ │ │ │ ├── DrawMode.kt │ │ │ │ │ │ ├── DrawType.kt │ │ │ │ │ │ ├── EditorDestination.kt │ │ │ │ │ │ ├── EditorItems.kt │ │ │ │ │ │ ├── ImageFilter.kt │ │ │ │ │ │ ├── MarkupItems.kt │ │ │ │ │ │ ├── PainterMotionEvent.kt │ │ │ │ │ │ ├── PathProperties.kt │ │ │ │ │ │ ├── SaveFormat.kt │ │ │ │ │ │ └── VariableFilter.kt │ │ │ │ ├── repository │ │ │ │ │ └── MediaRepository.kt │ │ │ │ ├── use_case │ │ │ │ │ └── MediaHandleUseCase.kt │ │ │ │ └── util │ │ │ │ │ ├── Converters.kt │ │ │ │ │ ├── MediaExt.kt │ │ │ │ │ ├── MediaOrder.kt │ │ │ │ │ ├── OrderType.kt │ │ │ │ │ └── UriSerializer.kt │ │ │ └── presentation │ │ │ │ ├── albums │ │ │ │ ├── AlbumsScreen.kt │ │ │ │ ├── AlbumsViewModel.kt │ │ │ │ └── components │ │ │ │ │ ├── AlbumComponent.kt │ │ │ │ │ └── PinnedAlbumsCarousel.kt │ │ │ │ ├── classifier │ │ │ │ ├── CategoriesScreen.kt │ │ │ │ ├── CategoriesViewModel.kt │ │ │ │ ├── CategoryViewModel.kt │ │ │ │ ├── CategoryViewScreen.kt │ │ │ │ ├── ClassifierWorker.kt │ │ │ │ └── ImageClassifierHelper.kt │ │ │ │ ├── common │ │ │ │ ├── ChanneledViewModel.kt │ │ │ │ ├── MediaScreen.kt │ │ │ │ ├── MediaViewModel.kt │ │ │ │ └── components │ │ │ │ │ ├── MediaGrid.kt │ │ │ │ │ ├── MediaGridView.kt │ │ │ │ │ ├── MediaImage.kt │ │ │ │ │ ├── MetadataCollectionStatus.kt │ │ │ │ │ ├── OptionSheet.kt │ │ │ │ │ ├── TimelineScroller.kt │ │ │ │ │ ├── TwoLinedDateToolbarTitle.kt │ │ │ │ │ ├── rememberHeaderOffset.kt │ │ │ │ │ └── rememberStickyHeaderItem.kt │ │ │ │ ├── dateformat │ │ │ │ └── DateFormatScreen.kt │ │ │ │ ├── edit │ │ │ │ ├── EditActivity.kt │ │ │ │ ├── EditScreen.kt │ │ │ │ ├── EditViewModel.kt │ │ │ │ ├── adjustments │ │ │ │ │ ├── Crop.kt │ │ │ │ │ ├── Flip.kt │ │ │ │ │ ├── Markup.kt │ │ │ │ │ ├── Rotate90CW.kt │ │ │ │ │ ├── filters │ │ │ │ │ │ ├── Cool.kt │ │ │ │ │ │ ├── ImageFilterTypes.kt │ │ │ │ │ │ ├── Monochrome.kt │ │ │ │ │ │ ├── Negative.kt │ │ │ │ │ │ ├── None.kt │ │ │ │ │ │ ├── Posterize.kt │ │ │ │ │ │ ├── Sepia.kt │ │ │ │ │ │ ├── Vintage.kt │ │ │ │ │ │ └── Warm.kt │ │ │ │ │ └── varfilter │ │ │ │ │ │ ├── Brightness.kt │ │ │ │ │ │ ├── Contrast.kt │ │ │ │ │ │ ├── Rotate.kt │ │ │ │ │ │ ├── Saturation.kt │ │ │ │ │ │ ├── Sharpness.kt │ │ │ │ │ │ └── VariableFilterTypes.kt │ │ │ │ ├── components │ │ │ │ │ ├── adjustment │ │ │ │ │ │ ├── AdjustScrubber.kt │ │ │ │ │ │ ├── AdjustSection.kt │ │ │ │ │ │ └── SelectableItem.kt │ │ │ │ │ ├── core │ │ │ │ │ │ ├── HorizontalScrubber.kt │ │ │ │ │ │ ├── SupportiveLayout.kt │ │ │ │ │ │ ├── SupportiveLazyGridLayout.kt │ │ │ │ │ │ ├── SupportiveLazyLayout.kt │ │ │ │ │ │ └── VerticalScrubber.kt │ │ │ │ │ ├── cropper │ │ │ │ │ │ └── CropperSection.kt │ │ │ │ │ ├── editor │ │ │ │ │ │ ├── EditorItem.kt │ │ │ │ │ │ ├── EditorNavigator.kt │ │ │ │ │ │ ├── EditorSelector.kt │ │ │ │ │ │ ├── ExternalEditor.kt │ │ │ │ │ │ └── ImageViewer.kt │ │ │ │ │ ├── filters │ │ │ │ │ │ └── FiltersSelector.kt │ │ │ │ │ └── markup │ │ │ │ │ │ ├── ColorBars.kt │ │ │ │ │ │ ├── MarkupColorSelector.kt │ │ │ │ │ │ ├── MarkupPainter.kt │ │ │ │ │ │ ├── MarkupPathPreview.kt │ │ │ │ │ │ ├── MarkupSelector.kt │ │ │ │ │ │ └── MarkupSizeSelector.kt │ │ │ │ └── utils │ │ │ │ │ ├── awaitDragMotionEvent.kt │ │ │ │ │ └── isApplied.kt │ │ │ │ ├── exif │ │ │ │ ├── AddAlbumSheet.kt │ │ │ │ ├── CopyMediaSheet.kt │ │ │ │ ├── MetadataEditSheet.kt │ │ │ │ └── MoveMediaSheet.kt │ │ │ │ ├── favorites │ │ │ │ ├── FavoriteScreen.kt │ │ │ │ └── components │ │ │ │ │ ├── EmptyFavorites.kt │ │ │ │ │ └── FavoriteNavActions.kt │ │ │ │ ├── ignored │ │ │ │ ├── IgnoredScreen.kt │ │ │ │ ├── IgnoredState.kt │ │ │ │ ├── IgnoredViewModel.kt │ │ │ │ └── setup │ │ │ │ │ ├── IgnoredSetup.kt │ │ │ │ │ ├── IgnoredSetupDestination.kt │ │ │ │ │ ├── IgnoredSetupStage.kt │ │ │ │ │ ├── IgnoredSetupState.kt │ │ │ │ │ ├── IgnoredSetupViewModel.kt │ │ │ │ │ ├── IgnoredType.kt │ │ │ │ │ ├── SelectAlbumSheet.kt │ │ │ │ │ └── screens │ │ │ │ │ ├── SetupConfirmationScreen.kt │ │ │ │ │ ├── SetupLabelScreen.kt │ │ │ │ │ ├── SetupLocationScreen.kt │ │ │ │ │ ├── SetupTypeRegexScreen.kt │ │ │ │ │ └── SetupTypeSelectionScreen.kt │ │ │ │ ├── library │ │ │ │ ├── LibraryScreen.kt │ │ │ │ ├── LibraryViewModel.kt │ │ │ │ └── components │ │ │ │ │ ├── LibrarySmallItem.kt │ │ │ │ │ └── dashedBorder.kt │ │ │ │ ├── main │ │ │ │ └── MainActivity.kt │ │ │ │ ├── mediaview │ │ │ │ ├── MediaViewScreen.kt │ │ │ │ └── components │ │ │ │ │ ├── AppBar.kt │ │ │ │ │ ├── BottomBar.kt │ │ │ │ │ ├── MediaInfo.kt │ │ │ │ │ ├── media │ │ │ │ │ ├── MediaPreviewComponent.kt │ │ │ │ │ └── ZoomablePagerImage.kt │ │ │ │ │ └── video │ │ │ │ │ ├── VideoDurationHeader.kt │ │ │ │ │ ├── VideoPlayer.kt │ │ │ │ │ ├── VideoPlayerController.kt │ │ │ │ │ └── createDecryptedVideoFile.kt │ │ │ │ ├── picker │ │ │ │ ├── PickerActivity.kt │ │ │ │ ├── PickerViewModel.kt │ │ │ │ └── components │ │ │ │ │ ├── PickerMediaScreen.kt │ │ │ │ │ └── PickerScreen.kt │ │ │ │ ├── search │ │ │ │ ├── MainSearchBar.kt │ │ │ │ └── components │ │ │ │ │ ├── HistoryItem.kt │ │ │ │ │ ├── SearchBarElevation.kt │ │ │ │ │ └── SearchHistory.kt │ │ │ │ ├── settings │ │ │ │ ├── SettingsScreen.kt │ │ │ │ └── components │ │ │ │ │ ├── AppHeader.kt │ │ │ │ │ └── SettingsItems.kt │ │ │ │ ├── setup │ │ │ │ └── SetupScreen.kt │ │ │ │ ├── standalone │ │ │ │ ├── StandaloneActivity.kt │ │ │ │ └── StandaloneViewModel.kt │ │ │ │ ├── support │ │ │ │ └── SupportSheet.kt │ │ │ │ ├── timeline │ │ │ │ ├── TimelineScreen.kt │ │ │ │ └── components │ │ │ │ │ └── TimelineNavActions.kt │ │ │ │ ├── trashed │ │ │ │ ├── TrashedScreen.kt │ │ │ │ └── components │ │ │ │ │ ├── AutoDeleteFooter.kt │ │ │ │ │ ├── EmptyTrash.kt │ │ │ │ │ ├── TrashDialog.kt │ │ │ │ │ ├── TrashedNavActions.kt │ │ │ │ │ └── TrashedViewBottomBar.kt │ │ │ │ ├── util │ │ │ │ ├── AppBottomSheetState.kt │ │ │ │ ├── ContextExt.kt │ │ │ │ ├── DateExt.kt │ │ │ │ ├── ExifMetadata.kt │ │ │ │ ├── FileUtils.kt │ │ │ │ ├── Geolocation.kt │ │ │ │ ├── ImageUtils.kt │ │ │ │ ├── IntExt.kt │ │ │ │ ├── LogExt.kt │ │ │ │ ├── MapBoxURL.kt │ │ │ │ ├── ModifierExt.kt │ │ │ │ ├── MultiSelectExt.kt │ │ │ │ ├── NavigationItem.kt │ │ │ │ ├── NetworkExt.kt │ │ │ │ ├── Screen.kt │ │ │ │ ├── SharedElementsExt.kt │ │ │ │ ├── StateExt.kt │ │ │ │ ├── ViewScreenConstants.kt │ │ │ │ ├── WindowInsetsControllerExt.kt │ │ │ │ └── launchMap.kt │ │ │ │ ├── vault │ │ │ │ ├── VaultDisplay.kt │ │ │ │ ├── VaultScreen.kt │ │ │ │ ├── VaultScreens.kt │ │ │ │ ├── VaultSetup.kt │ │ │ │ ├── VaultViewModel.kt │ │ │ │ ├── VaultWorker.kt │ │ │ │ ├── components │ │ │ │ │ ├── DeleteVaultSheet.kt │ │ │ │ │ ├── NewVaultSheet.kt │ │ │ │ │ ├── RestoreVaultSheet.kt │ │ │ │ │ └── SelectVaultSheet.kt │ │ │ │ └── utils │ │ │ │ │ └── BiometricManagerExt.kt │ │ │ │ └── wallpaper │ │ │ │ └── SetWallpaperActivity.kt │ │ │ ├── injection │ │ │ └── AppModule.kt │ │ │ └── ui │ │ │ ├── ComposeInitializer.kt │ │ │ ├── core │ │ │ ├── Icons.kt │ │ │ └── icons │ │ │ │ ├── Albums.kt │ │ │ │ ├── Encrypted.kt │ │ │ │ ├── Face.kt │ │ │ │ ├── InkHighlighter.kt │ │ │ │ ├── InkMarker.kt │ │ │ │ ├── Ink_eraser.kt │ │ │ │ ├── NoImage.kt │ │ │ │ ├── RegularExpression.kt │ │ │ │ ├── Star.kt │ │ │ │ └── Stylus.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Dimens.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── ic_donate.xml │ │ ├── ic_gallery_thumbnail.xml │ │ ├── ic_github.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_foreground_monochrome.xml │ │ ├── ic_media_manage.xml │ │ ├── ic_remove.xml │ │ ├── ic_scroll_arrow.xml │ │ ├── image_sample_1.webp │ │ ├── image_sample_2.webp │ │ ├── image_sample_3.webp │ │ ├── image_sample_4.webp │ │ └── rounded_border_tv.xml │ │ ├── layout │ │ ├── album_pin_frame.xml │ │ ├── view_photo_editor_image.xml │ │ └── view_photo_editor_text.xml │ │ ├── mipmap │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── values-af-rZA │ │ └── strings.xml │ │ ├── values-ar-rSA │ │ └── strings.xml │ │ ├── values-be-rBY │ │ └── strings.xml │ │ ├── values-bg-rBG │ │ └── strings.xml │ │ ├── values-bn-rBD │ │ └── strings.xml │ │ ├── values-ca-rES │ │ └── strings.xml │ │ ├── values-cs-rCZ │ │ └── strings.xml │ │ ├── values-da-rDK │ │ └── strings.xml │ │ ├── values-de-rDE │ │ └── strings.xml │ │ ├── values-el-rGR │ │ └── strings.xml │ │ ├── values-en-rUS │ │ └── strings.xml │ │ ├── values-es-rES │ │ └── strings.xml │ │ ├── values-fi-rFI │ │ └── strings.xml │ │ ├── values-fr-rFR │ │ └── strings.xml │ │ ├── values-ga-rIE │ │ └── strings.xml │ │ ├── values-gl-rES │ │ └── strings.xml │ │ ├── values-hi-rIN │ │ └── strings.xml │ │ ├── values-hr-rHR │ │ └── strings.xml │ │ ├── values-hu-rHU │ │ └── strings.xml │ │ ├── values-in-rID │ │ └── strings.xml │ │ ├── values-it-rIT │ │ └── strings.xml │ │ ├── values-iw-rIL │ │ └── strings.xml │ │ ├── values-ja-rJP │ │ └── strings.xml │ │ ├── values-ko-rKR │ │ └── strings.xml │ │ ├── values-lt-rLT │ │ └── strings.xml │ │ ├── values-ne-rNP │ │ └── strings.xml │ │ ├── values-night │ │ └── colors.xml │ │ ├── values-nl-rNL │ │ └── strings.xml │ │ ├── values-nn-rNO │ │ └── strings.xml │ │ ├── values-no-rNO │ │ └── strings.xml │ │ ├── values-or-rIN │ │ └── strings.xml │ │ ├── values-pa-rIN │ │ └── strings.xml │ │ ├── values-pl-rPL │ │ └── strings.xml │ │ ├── values-pt-rBR │ │ └── strings.xml │ │ ├── values-pt-rPT │ │ └── strings.xml │ │ ├── values-ro-rRO │ │ └── strings.xml │ │ ├── values-ru-rBY │ │ └── strings.xml │ │ ├── values-ru-rRU │ │ └── strings.xml │ │ ├── values-sr-rSP │ │ └── strings.xml │ │ ├── values-sv-rSE │ │ └── strings.xml │ │ ├── values-tr-rTR │ │ └── strings.xml │ │ ├── values-uk-rUA │ │ └── strings.xml │ │ ├── values-vi-rVN │ │ └── strings.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── filepaths.xml │ ├── release │ └── generated │ │ └── baselineProfiles │ │ ├── baseline-prof.txt │ │ └── startup-prof.txt │ └── test │ └── java │ └── com │ └── dot │ └── gallery │ └── ExampleUnitTest.kt ├── baselineProfile ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── dot │ └── baselineprofile │ ├── BaselineProfileGenerator.kt │ └── StartupBenchmarks.kt ├── build.gradle.kts ├── crowdin.yml ├── fastlane └── metadata │ └── android │ └── en-US │ ├── changelogs │ ├── 10216.txt │ ├── 20111.txt │ ├── 21000.txt │ ├── 21009.txt │ └── 30033.txt │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ └── short_description.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libs ├── cropper │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── cropper │ │ │ ├── CropModifier.kt │ │ │ ├── ImageCropper.kt │ │ │ ├── TouchRegion.kt │ │ │ ├── crop │ │ │ └── CropAgent.kt │ │ │ ├── draw │ │ │ ├── ImageDrawCanvas.kt │ │ │ └── Overlay.kt │ │ │ ├── image │ │ │ ├── ImageScope.kt │ │ │ └── ImageWithConstraints.kt │ │ │ ├── model │ │ │ ├── AspectRatios.kt │ │ │ ├── CropAspectRatio.kt │ │ │ ├── CropData.kt │ │ │ ├── CropFrame.kt │ │ │ ├── CropOutline.kt │ │ │ ├── CropOutlineContainer.kt │ │ │ └── CropOutlineProperties.kt │ │ │ ├── settings │ │ │ ├── CropDefaults.kt │ │ │ ├── CropFrameFactory.kt │ │ │ ├── CropType.kt │ │ │ └── Paths.kt │ │ │ ├── state │ │ │ ├── CropState.kt │ │ │ ├── CropStateImpl.kt │ │ │ ├── DynamicCropState.kt │ │ │ ├── StaticCropState.kt │ │ │ └── TransformState.kt │ │ │ ├── ui │ │ │ └── theme │ │ │ │ └── Color.kt │ │ │ ├── util │ │ │ ├── DimensionUtil.kt │ │ │ ├── DrawScopeUtils.kt │ │ │ ├── ImageContentScaleUtil.kt │ │ │ ├── OffsetUtil.kt │ │ │ ├── ShapeUtils.kt │ │ │ ├── ZoomLevel.kt │ │ │ └── ZoomUtil.kt │ │ │ └── widget │ │ │ ├── AspectRatioSlectionCard.kt │ │ │ ├── CropFrameDisplayCard.kt │ │ │ └── GridImageLayout.kt │ │ └── res │ │ └── drawable │ │ └── landscape2.jpg └── gesture │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── smarttoolfactory │ └── gesture │ ├── AwaitPointerMotionEvent.kt │ ├── MotionEvent.kt │ ├── PointerMotionModifier.kt │ ├── TouchDelegateModifier.kt │ └── TransformGesture.kt ├── play_release.patch ├── renovate.json ├── screenshots ├── items │ ├── community_banner.png │ ├── get-it-on-github.png │ ├── support_banner.png │ ├── support_paypal.png │ └── support_revolut.png └── preview.png ├── settings.gradle.kts ├── strip_allfiles_permission.sh └── strip_permission.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | custom: ['https://revolut.me/somaldoaca', 'https://www.paypal.com/paypalme/iacobionut01'] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] Title" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## [Delete before submission] Please use and reproduce the bug on the latest build from Github Actions (Artifacts) 11 | **Describe the bug** 12 | A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Smartphone (please complete the following information):** 28 | - Device: [e.g. Google Pixel 6] 29 | - OS: [e.g. Android 14] 30 | - Version [e.g. 1.1.0] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement/Feature request 3 | about: Suggest an idea for this project 4 | title: "[Enhancement] Title" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## [Delete before submission] Please use the latest build from Github Actions (Artifacts) 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | api.properties 12 | /.kotlin 13 | /.vscode -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | *.jks -------------------------------------------------------------------------------- /app/compose_compiler_config.conf: -------------------------------------------------------------------------------- 1 | com.github.panpf.sketch.Sketch 2 | com.github.panpf.sketch.PlatformContext 3 | com.github.panpf.sketch.drawable.DrawableEqualizer 4 | com.github.panpf.sketch.request.Image 5 | com.github.panpf.sketch.request.ImageOptions 6 | com.github.panpf.sketch.request.ImageRequest 7 | com.github.panpf.sketch.request.ImageResult 8 | com.github.panpf.sketch.state.ColorDrawableStateImage 9 | com.github.panpf.sketch.state.CurrentStateImage 10 | com.github.panpf.sketch.state.DrawableStateImage 11 | com.github.panpf.sketch.state.ErrorStateImage 12 | com.github.panpf.sketch.state.MemoryCacheStateImage 13 | com.github.panpf.sketch.state.IconAnimatableStateImage 14 | com.github.panpf.sketch.state.IconStateImage 15 | com.github.panpf.sketch.state.StateImage 16 | com.github.panpf.sketch.state.ThumbnailMemoryCacheStateImage 17 | com.github.panpf.sketch.util.ColorFetcher 18 | com.github.panpf.sketch.util.Equalizer 19 | com.github.panpf.sketch.util.IntColor 20 | com.github.panpf.sketch.util.Size -------------------------------------------------------------------------------- /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.kts. 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.bouncycastle.jsse.** 24 | -dontwarn org.conscrypt.** 25 | -dontwarn org.openjsse.** 26 | -dontwarn org.slf4j.impl.StaticLoggerBinder 27 | 28 | -keep class com.dot.gallery.feature_node.presentation.edit.adjustments.** { *; } 29 | -keep class com.drew.** { *; } 30 | -keep class java.io.** { *; } 31 | -keep class com.adobe.** { *; } 32 | 33 | -dontwarn com.google.auto.value.AutoValue$Builder 34 | -dontwarn com.google.auto.value.AutoValue 35 | -dontwarn org.tensorflow.lite.gpu.GpuDelegateFactory$Options$GpuBackend 36 | -dontwarn org.tensorflow.lite.gpu.GpuDelegateFactory$Options -------------------------------------------------------------------------------- /app/schemas/com.dot.gallery.feature_node.data.data_source.InternalDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "2fe35fcd34db546153a2ddbaa2e5e76a", 6 | "entities": [ 7 | { 8 | "tableName": "pinned_table", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | } 17 | ], 18 | "primaryKey": { 19 | "autoGenerate": false, 20 | "columnNames": [ 21 | "id" 22 | ] 23 | }, 24 | "indices": [], 25 | "foreignKeys": [] 26 | } 27 | ], 28 | "views": [], 29 | "setupQueries": [ 30 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 31 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2fe35fcd34db546153a2ddbaa2e5e76a')" 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /app/schemas/com.dot.gallery.feature_node.data.data_source.InternalDatabase/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 2, 5 | "identityHash": "57402321aff199c16e2f25a4a21623ba", 6 | "entities": [ 7 | { 8 | "tableName": "pinned_table", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | } 17 | ], 18 | "primaryKey": { 19 | "autoGenerate": false, 20 | "columnNames": [ 21 | "id" 22 | ] 23 | }, 24 | "indices": [], 25 | "foreignKeys": [] 26 | }, 27 | { 28 | "tableName": "blacklist", 29 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `label` TEXT NOT NULL, PRIMARY KEY(`id`))", 30 | "fields": [ 31 | { 32 | "fieldPath": "id", 33 | "columnName": "id", 34 | "affinity": "INTEGER", 35 | "notNull": true 36 | }, 37 | { 38 | "fieldPath": "label", 39 | "columnName": "label", 40 | "affinity": "TEXT", 41 | "notNull": true 42 | } 43 | ], 44 | "primaryKey": { 45 | "autoGenerate": false, 46 | "columnNames": [ 47 | "id" 48 | ] 49 | }, 50 | "indices": [], 51 | "foreignKeys": [] 52 | } 53 | ], 54 | "views": [], 55 | "setupQueries": [ 56 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 57 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '57402321aff199c16e2f25a4a21623ba')" 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /app/schemas/debug/com.dot.gallery.feature_node.data.data_source.InternalDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "2fe35fcd34db546153a2ddbaa2e5e76a", 6 | "entities": [ 7 | { 8 | "tableName": "pinned_table", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | } 17 | ], 18 | "primaryKey": { 19 | "autoGenerate": false, 20 | "columnNames": [ 21 | "id" 22 | ] 23 | }, 24 | "indices": [], 25 | "foreignKeys": [] 26 | } 27 | ], 28 | "views": [], 29 | "setupQueries": [ 30 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 31 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2fe35fcd34db546153a2ddbaa2e5e76a')" 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /app/schemas/release/com.dot.gallery.feature_node.data.data_source.InternalDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "2fe35fcd34db546153a2ddbaa2e5e76a", 6 | "entities": [ 7 | { 8 | "tableName": "pinned_table", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | } 17 | ], 18 | "primaryKey": { 19 | "autoGenerate": false, 20 | "columnNames": [ 21 | "id" 22 | ] 23 | }, 24 | "indices": [], 25 | "foreignKeys": [] 26 | } 27 | ], 28 | "views": [], 29 | "setupQueries": [ 30 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 31 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2fe35fcd34db546153a2ddbaa2e5e76a')" 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /app/schemas/staging/com.dot.gallery.feature_node.data.data_source.InternalDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "2fe35fcd34db546153a2ddbaa2e5e76a", 6 | "entities": [ 7 | { 8 | "tableName": "pinned_table", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "INTEGER", 15 | "notNull": true 16 | } 17 | ], 18 | "primaryKey": { 19 | "autoGenerate": false, 20 | "columnNames": [ 21 | "id" 22 | ] 23 | }, 24 | "indices": [], 25 | "foreignKeys": [] 26 | } 27 | ], 28 | "views": [], 29 | "setupQueries": [ 30 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 31 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2fe35fcd34db546153a2ddbaa2e5e76a')" 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/dot/gallery/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.dot.gallery", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/assets/mobile_ica_8bit_with_metadata.tflite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/app/src/main/assets/mobile_ica_8bit_with_metadata.tflite -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/Resource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.core 7 | 8 | sealed class Resource(var data: T? = null, val message: String? = null) { 9 | class Success(data: T) : Resource(data) 10 | class Error(message: String, data: T? = null) : Resource(data, message) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/presentation/components/CheckBox.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.core.presentation.components 7 | 8 | import androidx.compose.foundation.Image 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.filled.CheckCircle 11 | import androidx.compose.material.icons.outlined.Circle 12 | import androidx.compose.material3.IconButton 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.ColorFilter 17 | 18 | @Composable 19 | fun CheckBox( 20 | modifier: Modifier = Modifier, 21 | isChecked: Boolean, 22 | onCheck: (() -> Unit)? = null 23 | ) { 24 | val image = if (isChecked) Icons.Filled.CheckCircle else Icons.Outlined.Circle 25 | val color = if (isChecked) MaterialTheme.colorScheme.primary 26 | else MaterialTheme.colorScheme.onSurface 27 | if (onCheck != null) { 28 | IconButton( 29 | onClick = onCheck, 30 | modifier = modifier 31 | ) { 32 | Image( 33 | imageVector = image, 34 | colorFilter = ColorFilter.tint(color), 35 | contentDescription = null 36 | ) 37 | } 38 | } else { 39 | Image( 40 | imageVector = image, 41 | colorFilter = ColorFilter.tint(color), 42 | modifier = modifier, 43 | contentDescription = null 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/presentation/components/DragHandle.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.core.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun DragHandle() { 14 | Surface( 15 | modifier = Modifier 16 | .padding(vertical = 11.dp), 17 | color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), 18 | shape = MaterialTheme.shapes.extraLarge 19 | ) { 20 | Box(Modifier.size(width = 32.dp, height = 4.dp)) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/presentation/components/EmptyAlbum.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.core.presentation.components 7 | 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.text.style.TextAlign 16 | import androidx.compose.ui.unit.dp 17 | import com.dot.gallery.R 18 | 19 | @Composable 20 | fun EmptyAlbum( 21 | modifier: Modifier = Modifier, 22 | title: String = stringResource(R.string.no_media_title), 23 | ) = LoadingAlbum( 24 | modifier = modifier, 25 | shouldShimmer = false, 26 | bottomContent = { 27 | Text( 28 | modifier = Modifier.fillMaxWidth().padding(32.dp), 29 | text = title, 30 | style = MaterialTheme.typography.titleLarge, 31 | textAlign = TextAlign.Center 32 | ) 33 | } 34 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/presentation/components/EmptyMedia.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.core.presentation.components 7 | 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.text.style.TextAlign 16 | import androidx.compose.ui.unit.dp 17 | import com.dot.gallery.R 18 | 19 | @Composable 20 | fun EmptyMedia( 21 | modifier: Modifier = Modifier, 22 | title: String = stringResource(R.string.no_media_title), 23 | ) = LoadingMedia( 24 | modifier = modifier, 25 | shouldShimmer = false, 26 | bottomContent = { 27 | Text( 28 | modifier = Modifier.fillMaxWidth().padding(32.dp), 29 | text = title, 30 | style = MaterialTheme.typography.titleLarge, 31 | textAlign = TextAlign.Center 32 | ) 33 | } 34 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/presentation/components/NavigationActions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.core.presentation.components 7 | 8 | import androidx.activity.compose.rememberLauncherForActivityResult 9 | import androidx.activity.result.ActivityResult 10 | import androidx.activity.result.ActivityResultLauncher 11 | import androidx.activity.result.IntentSenderRequest 12 | import androidx.activity.result.contract.ActivityResultContracts 13 | import androidx.compose.foundation.layout.RowScope 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.MutableState 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.saveable.rememberSaveable 18 | 19 | @Composable 20 | fun RowScope.NavigationActions( 21 | actions: @Composable (RowScope.(expandedDropDown: MutableState, result: ActivityResultLauncher) -> Unit), 22 | onActivityResult: (result: ActivityResult) -> Unit 23 | ) { 24 | val expandedDropDown = rememberSaveable { mutableStateOf(false) } 25 | val result = rememberLauncherForActivityResult( 26 | contract = ActivityResultContracts.StartIntentSenderForResult(), 27 | onResult = { 28 | onActivityResult(it) 29 | } 30 | ) 31 | actions(expandedDropDown, result) 32 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/presentation/components/NavigationBarSpacer.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.core.presentation.components 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import com.dot.gallery.feature_node.presentation.util.getNavigationBarHeight 9 | 10 | @Composable 11 | fun NavigationBarSpacer() { 12 | Spacer( 13 | modifier = Modifier 14 | .fillMaxWidth() 15 | .height(getNavigationBarHeight()) 16 | ) 17 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/presentation/components/NavigationButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.core.presentation.components 7 | 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 10 | import androidx.compose.material.icons.filled.Close 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.IconButton 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.MutableState 15 | import androidx.compose.ui.res.stringResource 16 | import com.dot.gallery.R 17 | 18 | @Composable 19 | fun NavigationButton( 20 | albumId: Long, 21 | target: String?, 22 | navigateUp: () -> Unit, 23 | clearSelection: () -> Unit, 24 | selectionState: MutableState, 25 | alwaysGoBack: Boolean, 26 | ) { 27 | val isChildRoute = albumId != -1L || target != null 28 | val onClick: () -> Unit = 29 | if (isChildRoute && !selectionState.value) navigateUp 30 | else clearSelection 31 | val icon = if (isChildRoute && !selectionState.value) Icons.AutoMirrored.Filled.ArrowBack 32 | else Icons.Default.Close 33 | if (isChildRoute || selectionState.value || alwaysGoBack) { 34 | IconButton(onClick = onClick) { 35 | Icon( 36 | imageVector = icon, 37 | contentDescription = stringResource(R.string.back_cd) 38 | ) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/OnLifecycleEvent.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.core.presentation.components.util 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.runtime.rememberUpdatedState 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.LifecycleEventObserver 8 | import androidx.lifecycle.LifecycleOwner 9 | import androidx.lifecycle.compose.LocalLifecycleOwner 10 | 11 | @Composable 12 | fun OnLifecycleEvent(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit) { 13 | val eventHandler = rememberUpdatedState(onEvent) 14 | val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current) 15 | 16 | DisposableEffect(lifecycleOwner.value) { 17 | val lifecycle = lifecycleOwner.value.lifecycle 18 | val observer = LifecycleEventObserver { owner, event -> 19 | eventHandler.value(owner, event) 20 | } 21 | 22 | lifecycle.addObserver(observer) 23 | onDispose { 24 | lifecycle.removeObserver(observer) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/PermissionsExt.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.core.presentation.components.util 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | 6 | fun Context.permissionGranted(list: List): Boolean { 7 | var granted = true 8 | list.forEach { 9 | if (checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED) granted = false 10 | } 11 | return granted 12 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/presentation/components/util/ShadowModifier.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.core.presentation.components.util 7 | 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.draw.drawBehind 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.graphics.Paint 12 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.unit.Dp 15 | import androidx.compose.ui.unit.dp 16 | 17 | fun Modifier.advancedShadow( 18 | color: Color = Color.Black, 19 | alpha: Float = 1f, 20 | cornersRadius: Dp = 0.dp, 21 | shadowBlurRadius: Dp = 0.dp, 22 | offsetY: Dp = 0.dp, 23 | offsetX: Dp = 0.dp 24 | ) = drawBehind { 25 | 26 | val shadowColor = color.copy(alpha = alpha).toArgb() 27 | val transparentColor = color.copy(alpha = 0f).toArgb() 28 | 29 | drawIntoCanvas { 30 | val paint = Paint() 31 | val frameworkPaint = paint.asFrameworkPaint() 32 | frameworkPaint.color = transparentColor 33 | frameworkPaint.setShadowLayer( 34 | shadowBlurRadius.toPx(), 35 | offsetX.toPx(), 36 | offsetY.toPx(), 37 | shadowColor 38 | ) 39 | it.drawRoundRect( 40 | 0f, 41 | 0f, 42 | this.size.width, 43 | this.size.height, 44 | cornersRadius.toPx(), 45 | cornersRadius.toPx(), 46 | paint 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/util/Column.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.core.util 2 | 3 | typealias Column = String 4 | 5 | sealed interface Node { 6 | fun build(): String = when (this) { 7 | is Eq -> "${lhs.build()} = ${rhs.build()}" 8 | is Or -> "(${lhs.build()}) OR (${rhs.build()})" 9 | is And -> "(${lhs.build()}) AND (${rhs.build()})" 10 | is Literal<*> -> "$`val`" 11 | } 12 | } 13 | 14 | private class Eq(val lhs: Node, val rhs: Node) : Node 15 | private class Or(val lhs: Node, val rhs: Node) : Node 16 | private class And(val lhs: Node, val rhs: Node) : Node 17 | private class Literal(val `val`: T) : Node 18 | 19 | class Query(val root: Node) { 20 | fun build() = root.build() 21 | 22 | companion object { 23 | const val ARG = "?" 24 | } 25 | } 26 | 27 | infix fun Query.or(other: Query) = Query(Or(this.root, other.root)) 28 | infix fun Query.and(other: Query) = Query(And(this.root, other.root)) 29 | infix fun Query.eq(other: Query) = Query(Eq(this.root, other.root)) 30 | infix fun Column.eq(other: T) = Query(Literal(this)) eq Query(Literal(other)) 31 | 32 | fun Iterable.join( 33 | func: Query.(other: Query) -> Query, 34 | ) = reduceOrNull(func) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/util/MediaStoreBuckets.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 The LineageOS Project 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | package com.dot.gallery.core.util 6 | 7 | enum class MediaStoreBuckets { 8 | /** 9 | * Favorites album. 10 | */ 11 | MEDIA_STORE_BUCKET_FAVORITES, 12 | 13 | /** 14 | * Trash album. 15 | */ 16 | MEDIA_STORE_BUCKET_TRASH, 17 | 18 | /** 19 | * Timeline, contains all medias. 20 | */ 21 | MEDIA_STORE_BUCKET_TIMELINE, 22 | 23 | /** 24 | * Reserved bucket ID for placeholders, throw an exception if this value is used. 25 | */ 26 | MEDIA_STORE_BUCKET_PLACEHOLDER, 27 | 28 | /** 29 | * Timeline, contains only photos. 30 | */ 31 | MEDIA_STORE_BUCKET_PHOTOS, 32 | 33 | /** 34 | * Timeline, contains only videos. 35 | */ 36 | MEDIA_STORE_BUCKET_VIDEOS; 37 | 38 | val id = (-0x0000DEAD - ((ordinal + 1) shl 16)).toLong() 39 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/util/PickerUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.core.util 2 | 3 | import android.content.Intent 4 | import android.provider.MediaStore 5 | import com.dot.gallery.feature_node.domain.model.MediaType 6 | 7 | object PickerUtils { 8 | private const val MIME_TYPE_IMAGE_ANY = "image/*" 9 | private const val MIME_TYPE_VIDEO_ANY = "video/*" 10 | private const val MIME_TYPE_ANY = "*/*" 11 | 12 | /** 13 | * Get a fixed up MIME type from an [Intent]. 14 | * @param intent An [Intent] 15 | * @return A simpler MIME type, null if not supported 16 | */ 17 | fun translateMimeType(intent: Intent?) = when (intent?.action) { 18 | Intent.ACTION_SET_WALLPAPER -> MIME_TYPE_IMAGE_ANY 19 | else -> (intent?.type ?: MIME_TYPE_ANY).let { 20 | when (it) { 21 | MediaStore.Images.Media.CONTENT_TYPE -> MIME_TYPE_IMAGE_ANY 22 | MediaStore.Video.Media.CONTENT_TYPE -> MIME_TYPE_VIDEO_ANY 23 | else -> when { 24 | it == MIME_TYPE_ANY 25 | || it.startsWith("image/") 26 | || it.startsWith("video/") -> it 27 | 28 | else -> null 29 | } 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * Get a [MediaType] only if the provided MIME type is a generic one, else return null. 36 | * @param mimeType A MIME type 37 | * @return [MediaType] if the MIME type is generic, else null 38 | * (assume MIME type represent either a specific file format or any) 39 | */ 40 | fun mediaTypeFromGenericMimeType(mimeType: String?) = when (mimeType) { 41 | MIME_TYPE_IMAGE_ANY -> MediaType.IMAGE 42 | MIME_TYPE_VIDEO_ANY -> MediaType.VIDEO 43 | else -> null 44 | } 45 | 46 | /** 47 | * Given a MIME type, check if it specifies both a content type and a sub type. 48 | * @param mimeType A MIME type 49 | * @return true if it specifies both a file category and a specific type 50 | */ 51 | fun isMimeTypeNotGeneric(mimeType: String?) = mimeType?.let { 52 | it !in listOf( 53 | MIME_TYPE_IMAGE_ANY, 54 | MIME_TYPE_VIDEO_ANY, 55 | MIME_TYPE_ANY, 56 | ) 57 | } ?: false 58 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/util/ext/CursorExt.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 The LineageOS Project 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.core.util.ext 7 | 8 | import android.database.Cursor 9 | import androidx.core.database.getLongOrNull 10 | import androidx.core.database.getStringOrNull 11 | import com.dot.gallery.core.Resource 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.flow.flowOn 15 | import kotlinx.coroutines.flow.map 16 | 17 | fun Cursor?.mapEachRow( 18 | projection: Array, 19 | mapping: (Cursor, Array) -> T, 20 | ) = this?.use { cursor -> 21 | if (!cursor.moveToFirst()) { 22 | return@use emptyList() 23 | } 24 | 25 | val indexCache = projection.map { column -> 26 | cursor.getColumnIndexOrThrow(column) 27 | }.toTypedArray() 28 | 29 | val data = mutableListOf() 30 | do { 31 | data.add(mapping(cursor, indexCache)) 32 | } while (cursor.moveToNext()) 33 | 34 | data.toList() 35 | } ?: emptyList() 36 | 37 | fun Cursor?.tryGetString(columnIndex: Int, fallback: String? = null): String? { 38 | return this?.getStringOrNull(columnIndex) ?: fallback 39 | } 40 | 41 | fun Cursor?.tryGetLong(columnIndex: Int, fallback: Long? = null): Long? { 42 | return this?.getLongOrNull(columnIndex) ?: fallback 43 | } 44 | 45 | fun Flow>.mapAsResource(errorOnEmpty: Boolean = false, errorMessage: String = "No data found") = map { 46 | if (errorOnEmpty && it.isEmpty()) { 47 | Resource.Error(errorMessage) 48 | } else { 49 | Resource.Success(it) 50 | } 51 | }.flowOn(Dispatchers.IO) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/core/util/ext/FlowExt.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 The LineageOS Project 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.core.util.ext 7 | 8 | import android.database.Cursor 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.map 11 | 12 | fun Flow.mapEachRow( 13 | projection: Array, 14 | mapping: (Cursor, Array) -> T, 15 | ) = map { it.mapEachRow(projection, mapping) } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/BlacklistDao.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.data.data_source 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Query 6 | import androidx.room.Upsert 7 | import com.dot.gallery.feature_node.domain.model.IgnoredAlbum 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | @Dao 11 | interface BlacklistDao { 12 | 13 | @Query("SELECT * FROM blacklist") 14 | fun getBlacklistedAlbums(): Flow> 15 | 16 | @Query("SELECT * FROM blacklist") 17 | suspend fun getBlacklistedAlbumsAsync(): List 18 | 19 | @Upsert 20 | suspend fun addBlacklistedAlbum(ignoredAlbum: IgnoredAlbum) 21 | 22 | @Delete 23 | suspend fun removeBlacklistedAlbum(ignoredAlbum: IgnoredAlbum) 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/InternalDatabase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.data.data_source 7 | 8 | import androidx.room.AutoMigration 9 | import androidx.room.Database 10 | import androidx.room.RoomDatabase 11 | import androidx.room.TypeConverters 12 | import com.dot.gallery.feature_node.domain.model.IgnoredAlbum 13 | import com.dot.gallery.feature_node.domain.model.Media 14 | import com.dot.gallery.feature_node.domain.model.MediaMetadataCore 15 | import com.dot.gallery.feature_node.domain.model.MediaMetadataFlags 16 | import com.dot.gallery.feature_node.domain.model.MediaMetadataVideo 17 | import com.dot.gallery.feature_node.domain.model.MediaVersion 18 | import com.dot.gallery.feature_node.domain.model.PinnedAlbum 19 | import com.dot.gallery.feature_node.domain.model.TimelineSettings 20 | import com.dot.gallery.feature_node.domain.model.Vault 21 | import com.dot.gallery.feature_node.domain.util.Converters 22 | 23 | @Database( 24 | entities = [ 25 | PinnedAlbum::class, 26 | IgnoredAlbum::class, 27 | Media.UriMedia::class, 28 | MediaVersion::class, 29 | TimelineSettings::class, 30 | Media.ClassifiedMedia::class, 31 | Media.EncryptedMedia2::class, 32 | Vault::class, 33 | MediaMetadataCore::class, 34 | MediaMetadataVideo::class, 35 | MediaMetadataFlags::class 36 | ], 37 | version = 8, 38 | exportSchema = true, 39 | autoMigrations = [ 40 | AutoMigration(from = 1, to = 2), 41 | AutoMigration(from = 2, to = 3), 42 | AutoMigration(from = 3, to = 4), 43 | AutoMigration(from = 4, to = 5), 44 | AutoMigration(from = 5, to = 6), 45 | AutoMigration(from = 6, to = 7), 46 | AutoMigration(from = 7, to = 8) 47 | ] 48 | ) 49 | @TypeConverters(Converters::class) 50 | abstract class InternalDatabase : RoomDatabase() { 51 | 52 | abstract fun getPinnedDao(): PinnedDao 53 | 54 | abstract fun getBlacklistDao(): BlacklistDao 55 | 56 | abstract fun getMediaDao(): MediaDao 57 | 58 | abstract fun getClassifierDao(): ClassifierDao 59 | 60 | abstract fun getVaultDao(): VaultDao 61 | 62 | abstract fun getMetadataDao(): MetadataDao 63 | 64 | companion object { 65 | const val NAME = "internal_db" 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/MetadataDao.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.data.data_source 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import androidx.room.Transaction 6 | import androidx.room.Upsert 7 | import com.dot.gallery.feature_node.domain.model.FullMediaMetadata 8 | import com.dot.gallery.feature_node.domain.model.MediaMetadata 9 | import com.dot.gallery.feature_node.domain.model.MediaMetadataCore 10 | import com.dot.gallery.feature_node.domain.model.MediaMetadataFlags 11 | import com.dot.gallery.feature_node.domain.model.MediaMetadataVideo 12 | import com.dot.gallery.feature_node.domain.model.toCore 13 | import com.dot.gallery.feature_node.domain.model.toFlags 14 | import com.dot.gallery.feature_node.domain.model.toVideo 15 | import kotlinx.coroutines.flow.Flow 16 | 17 | @Dao 18 | interface MetadataDao { 19 | 20 | @Transaction 21 | fun addMetadata(mediaMetadata: MediaMetadata) { 22 | upsertCore(mediaMetadata.toCore()) 23 | upsertVideo(mediaMetadata.toVideo()) 24 | upsertFlags(mediaMetadata.toFlags()) 25 | } 26 | 27 | @Upsert fun upsertCore(core: MediaMetadataCore) 28 | @Upsert fun upsertVideo(video: MediaMetadataVideo) 29 | @Upsert fun upsertFlags(flags: MediaMetadataFlags) 30 | 31 | @Transaction 32 | suspend fun deleteForgottenMetadata(ids: List) { 33 | deleteOrphansCore(ids) 34 | deleteOrphansVideo(ids) 35 | deleteOrphansFlags(ids) 36 | } 37 | 38 | @Query("DELETE FROM media_metadata_core WHERE mediaId NOT IN (:ids)") 39 | suspend fun deleteOrphansCore(ids: List) 40 | 41 | @Query("DELETE FROM media_metadata_video WHERE mediaId NOT IN (:ids)") 42 | suspend fun deleteOrphansVideo(ids: List) 43 | 44 | @Query("DELETE FROM media_metadata_flags WHERE mediaId NOT IN (:ids)") 45 | suspend fun deleteOrphansFlags(ids: List) 46 | 47 | @Transaction 48 | @Query("SELECT * FROM media_metadata_core") 49 | fun getFullMetadata(): Flow> 50 | 51 | @Transaction 52 | @Query("SELECT * FROM media_metadata_core WHERE mediaId = :id") 53 | fun getFullMetadata(id: Long): Flow 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/PinnedDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.data.data_source 7 | 8 | import androidx.room.Dao 9 | import androidx.room.Delete 10 | import androidx.room.Query 11 | import androidx.room.Upsert 12 | import com.dot.gallery.feature_node.domain.model.PinnedAlbum 13 | import kotlinx.coroutines.flow.Flow 14 | 15 | @Dao 16 | interface PinnedDao { 17 | 18 | @Query("SELECT * FROM pinned_table") 19 | fun getPinnedAlbums(): Flow> 20 | 21 | @Upsert 22 | suspend fun insertPinnedAlbum(pinnedAlbum: PinnedAlbum) 23 | 24 | @Delete 25 | suspend fun removePinnedAlbum(pinnedAlbum: PinnedAlbum) 26 | 27 | @Query("SELECT EXISTS(SELECT * FROM pinned_table WHERE id = :albumId)") 28 | fun albumIsPinned(albumId: Long): Boolean 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/VaultDao.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.data.data_source 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Query 6 | import androidx.room.Transaction 7 | import androidx.room.Upsert 8 | import com.dot.gallery.feature_node.domain.model.Media 9 | import com.dot.gallery.feature_node.domain.model.Vault 10 | import kotlinx.coroutines.flow.Flow 11 | import java.util.UUID 12 | 13 | @Dao 14 | interface VaultDao { 15 | 16 | /** 17 | * Vault Management 18 | */ 19 | 20 | @Query("SELECT * FROM vaults") 21 | fun getVaults(): Flow> 22 | 23 | @Upsert 24 | suspend fun insertVault(vault: Vault) 25 | 26 | @Delete 27 | suspend fun deleteVaultInfo(vault: Vault) 28 | 29 | @Transaction 30 | suspend fun deleteVault(vault: Vault) { 31 | deleteAllMediaFromVault(vault.uuid) 32 | deleteVaultInfo(vault) 33 | } 34 | 35 | @Query("DELETE FROM vaults") 36 | suspend fun deleteAllVaults() 37 | 38 | /** 39 | * Vault Media Management 40 | */ 41 | 42 | @Query("SELECT * FROM encrypted_media") 43 | fun getAllMedia(): Flow> 44 | 45 | @Query("SELECT * FROM encrypted_media WHERE uuid = :uuid") 46 | fun getMediaFromVault(uuid: UUID?): Flow> 47 | 48 | @Upsert 49 | suspend fun addMediaToVault(media: Media.EncryptedMedia2) 50 | 51 | @Query("DELETE FROM encrypted_media WHERE uuid = :uuid AND id = :id") 52 | suspend fun deleteMediaFromVault(uuid: UUID, id: Long): Int 53 | 54 | @Transaction 55 | suspend fun deleteMediaFromVault(media: Media.EncryptedMedia2): Boolean { 56 | return deleteMediaFromVault(uuid = media.uuid, id = media.id) > 0 57 | } 58 | 59 | @Query("DELETE FROM encrypted_media WHERE uuid = :uuid") 60 | suspend fun deleteAllMediaFromVault(uuid: UUID) 61 | 62 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/MediaQuery.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.data.data_source.mediastore 2 | 3 | import android.net.Uri 4 | import android.provider.MediaStore 5 | import com.dot.gallery.core.util.eq 6 | import com.dot.gallery.core.util.or 7 | 8 | object MediaQuery { 9 | val MediaStoreFileUri: Uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) 10 | val MediaProjection = arrayOf( 11 | MediaStore.Files.FileColumns._ID, 12 | MediaStore.Files.FileColumns.DATA, 13 | MediaStore.Files.FileColumns.RELATIVE_PATH, 14 | MediaStore.Files.FileColumns.DISPLAY_NAME, 15 | MediaStore.Files.FileColumns.BUCKET_ID, 16 | MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME, 17 | MediaStore.Files.FileColumns.DATE_TAKEN, 18 | MediaStore.Files.FileColumns.DATE_MODIFIED, 19 | MediaStore.Files.FileColumns.DURATION, 20 | MediaStore.Files.FileColumns.SIZE, 21 | MediaStore.Files.FileColumns.MIME_TYPE, 22 | MediaStore.Files.FileColumns.IS_FAVORITE, 23 | MediaStore.Files.FileColumns.IS_TRASHED, 24 | MediaStore.Files.FileColumns.DATE_EXPIRES 25 | ) 26 | val AlbumsProjection = arrayOf( 27 | MediaStore.Files.FileColumns._ID, 28 | MediaStore.Files.FileColumns.DATA, 29 | MediaStore.Files.FileColumns.RELATIVE_PATH, 30 | MediaStore.Files.FileColumns.DISPLAY_NAME, 31 | MediaStore.Files.FileColumns.BUCKET_ID, 32 | MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME, 33 | MediaStore.Files.FileColumns.DATE_TAKEN, 34 | MediaStore.Files.FileColumns.DATE_MODIFIED, 35 | MediaStore.Files.FileColumns.SIZE, 36 | MediaStore.Files.FileColumns.MIME_TYPE, 37 | ) 38 | 39 | object Selection { 40 | val image = 41 | MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE 42 | val video = 43 | MediaStore.Files.FileColumns.MEDIA_TYPE eq MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO 44 | val imageOrVideo = image or video 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/data/data_source/mediastore/queries/QueryFlow.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 The LineageOS Project 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | package com.dot.gallery.feature_node.data.data_source.mediastore.queries 6 | 7 | import android.database.Cursor 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | /** 11 | * Query flow 12 | * 13 | * This class is responsible for fetching data with a cursor 14 | * 15 | * @param T 16 | * @constructor Create empty Query flow 17 | */ 18 | abstract class QueryFlow { 19 | 20 | /** A flow of the data specified by the query */ 21 | abstract fun flowData(): Flow> 22 | 23 | /** A flow of the cursor specified by the query */ 24 | abstract fun flowCursor(): Flow 25 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/Album.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | package com.dot.gallery.feature_node.domain.model 6 | 7 | import android.net.Uri 8 | import android.os.Parcelable 9 | import androidx.compose.runtime.Stable 10 | import androidx.compose.ui.text.intl.Locale 11 | import androidx.compose.ui.text.toLowerCase 12 | import kotlinx.parcelize.IgnoredOnParcel 13 | import kotlinx.parcelize.Parcelize 14 | 15 | @Stable 16 | @Parcelize 17 | data class Album( 18 | val id: Long = 0, 19 | val label: String, 20 | val uri: Uri, 21 | val pathToThumbnail: String, 22 | val relativePath: String, 23 | val timestamp: Long, 24 | var count: Long = 0, 25 | var size: Long = 0, 26 | val isPinned: Boolean = false, 27 | ) : Parcelable { 28 | 29 | val key: String 30 | get() = "{$id, $uri, $timestamp}" 31 | 32 | val idLessKey: String 33 | get() = "{$uri, $timestamp}" 34 | 35 | @IgnoredOnParcel 36 | @Stable 37 | val volume: String = pathToThumbnail.substringBeforeLast("/").removeSuffix(relativePath.removeSuffix("/")) 38 | 39 | @IgnoredOnParcel 40 | @Stable 41 | val isOnSdcard: Boolean = 42 | volume.toLowerCase(Locale.current).matches(".*[0-9a-f]{4}-[0-9a-f]{4}".toRegex()) 43 | 44 | companion object { 45 | 46 | val NewAlbum = Album( 47 | id = -200, 48 | label = "New Album", 49 | uri = Uri.EMPTY, 50 | pathToThumbnail = "", 51 | relativePath = "", 52 | timestamp = 0 53 | ) 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/AlbumState.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | import androidx.compose.runtime.Stable 4 | 5 | @Stable 6 | data class AlbumState( 7 | val albums: List = emptyList(), 8 | val albumsWithBlacklisted: List = emptyList(), 9 | val albumsUnpinned: List = emptyList(), 10 | val albumsPinned: List = emptyList(), 11 | val error: String = "", 12 | val isLoading: Boolean = true 13 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/InfoRow.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.ui.graphics.vector.ImageVector 5 | 6 | @Stable 7 | data class InfoRow( 8 | val label: String, 9 | val content: String, 10 | val icon: ImageVector, 11 | val trailingIcon: ImageVector? = null, 12 | val contentDescription: String? = null, 13 | val onClick: (() -> Unit)? = null, 14 | val onLongClick: (() -> Unit)? = null, 15 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/LibraryIndicatorState.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | import androidx.compose.runtime.Stable 4 | import kotlinx.serialization.Serializable 5 | 6 | @Stable 7 | @Serializable 8 | data class LibraryIndicatorState( 9 | val trashCount: Int = 0, 10 | val favoriteCount: Int = 0, 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/LocationData.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.Stable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import com.dot.gallery.feature_node.presentation.util.formattedAddress 11 | import com.dot.gallery.feature_node.presentation.util.getLocation 12 | import com.dot.gallery.feature_node.presentation.util.rememberGeocoder 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.withContext 15 | 16 | @Stable 17 | data class LocationData( 18 | val latitude: Double, 19 | val longitude: Double, 20 | val location: String 21 | ) 22 | 23 | @Composable 24 | fun rememberLocationData( 25 | exifMetadata: MediaMetadata? 26 | ): LocationData? { 27 | val geocoder = rememberGeocoder() 28 | var locationName by remember { mutableStateOf(exifMetadata?.formattedCords) } 29 | LaunchedEffect(geocoder, exifMetadata) { 30 | withContext(Dispatchers.IO) { 31 | if (exifMetadata?.gpsLongitude != null && exifMetadata.gpsLatitude != null) { 32 | geocoder?.getLocation( 33 | exifMetadata.gpsLatitude, 34 | exifMetadata.gpsLongitude 35 | ) { address -> 36 | address?.let { 37 | val addressName = it.formattedAddress 38 | if (addressName.isNotEmpty()) { 39 | locationName = addressName 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | return remember(exifMetadata, locationName) { 47 | exifMetadata?.let { 48 | if (it.gpsLatitude == null || it.gpsLongitude == null) return@let null 49 | LocationData( 50 | latitude = it.gpsLatitude, 51 | longitude = it.gpsLongitude, 52 | location = locationName ?: "Unknown" 53 | ) 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaDateCaption.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.res.stringResource 8 | import com.dot.gallery.R 9 | import com.dot.gallery.core.Settings.Misc.rememberExifDateFormat 10 | import com.dot.gallery.feature_node.presentation.util.getDate 11 | 12 | @Stable 13 | data class MediaDateCaption( 14 | val date: String, 15 | val deviceInfo: String? = null, 16 | val description: String 17 | ) 18 | 19 | @Composable 20 | fun rememberMediaDateCaption( 21 | exifMetadata: MediaMetadata?, 22 | media: Media 23 | ): MediaDateCaption { 24 | val deviceInfo = remember(exifMetadata, media) { exifMetadata?.lensDescription } 25 | val defaultDesc = stringResource(R.string.image_add_description) 26 | val description = remember(exifMetadata, media) { exifMetadata?.imageDescription ?: defaultDesc } 27 | val currentDateFormat by rememberExifDateFormat() 28 | return remember(media, exifMetadata, currentDateFormat) { 29 | MediaDateCaption( 30 | date = media.definedTimestamp.getDate(currentDateFormat), 31 | deviceInfo = deviceInfo, 32 | description = description 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaItem.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | package com.dot.gallery.feature_node.domain.model 6 | 7 | import androidx.compose.runtime.Stable 8 | 9 | @Stable 10 | sealed class MediaItem { 11 | abstract val key: String 12 | 13 | @Stable 14 | data class Header ( 15 | override val key: String, 16 | val text: String, 17 | val data: Set 18 | ) : MediaItem() 19 | 20 | @Stable 21 | data class MediaViewItem ( 22 | override val key: String, 23 | val media: T 24 | ) : MediaItem() 25 | 26 | } 27 | 28 | val Any.isHeaderKey: Boolean 29 | get() = this is String && this.startsWith("header_") 30 | 31 | val Any.isBigHeaderKey: Boolean 32 | get() = this is String && this.startsWith("header_big_") 33 | 34 | val Any.isIgnoredKey: Boolean 35 | get() = this is String && this.contains("aboveGrid") -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaMetadataState.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | data class MediaMetadataState( 4 | val metadata: List = emptyList(), 5 | val isLoading: Boolean = false, 6 | val isLoadingProgress: Int = 0, 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaState.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | import androidx.compose.runtime.Stable 4 | 5 | @Stable 6 | data class MediaState( 7 | val media: List = emptyList(), 8 | val mappedMedia: List> = emptyList(), 9 | val mappedMediaWithMonthly: List> = emptyList(), 10 | val headers: List> = emptyList(), 11 | val dateHeader: String = "", 12 | val error: String = "", 13 | val isLoading: Boolean = true 14 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaType.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | import android.net.Uri 4 | import android.provider.MediaStore 5 | 6 | enum class MediaType( 7 | val externalContentUri: Uri, 8 | val mediaStoreValue: Int, 9 | ) { 10 | IMAGE( 11 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 12 | MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE, 13 | ), 14 | VIDEO( 15 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 16 | MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO, 17 | ); 18 | 19 | companion object { 20 | private val dashMimeTypes = listOf( 21 | "application/dash+xml", 22 | ) 23 | 24 | private val hlsMimeTypes = listOf( 25 | "application/vnd.apple.mpegurl", 26 | "application/x-mpegurl", 27 | "audio/mpegurl", 28 | "audio/x-mpegurl", 29 | ) 30 | 31 | private val smoothStreamingMimeTypes = listOf( 32 | "application/vnd.ms-sstr+xml", 33 | ) 34 | 35 | fun fromMediaStoreValue(value: Int) = entries.first { 36 | value == it.mediaStoreValue 37 | } 38 | 39 | fun fromMimeType(mimeType: String) = when { 40 | mimeType.startsWith("image/") -> IMAGE 41 | mimeType.startsWith("video/") -> VIDEO 42 | mimeType in dashMimeTypes -> VIDEO 43 | mimeType in hlsMimeTypes -> VIDEO 44 | mimeType in smoothStreamingMimeTypes -> VIDEO 45 | else -> null 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/MediaVersion.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | import androidx.room.Entity 4 | 5 | @Entity(tableName = "media_version", primaryKeys = ["version"]) 6 | data class MediaVersion( 7 | val version: String 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/PinnedAlbum.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | package com.dot.gallery.feature_node.domain.model 6 | 7 | import androidx.compose.runtime.Immutable 8 | import androidx.room.Entity 9 | import androidx.room.PrimaryKey 10 | 11 | @Entity(tableName = "pinned_table") 12 | @Immutable 13 | data class PinnedAlbum( 14 | @PrimaryKey(autoGenerate = false) 15 | val id: Long 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/PlaybackSpeed.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | data class PlaybackSpeed( 4 | val speed: Float, 5 | val label: String, 6 | val isAuto: Boolean = false 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/TimelineSettings.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.PrimaryKey 7 | import com.dot.gallery.feature_node.domain.util.MediaOrder 8 | import com.dot.gallery.feature_node.domain.util.OrderType 9 | 10 | /** 11 | * Timeline settings 12 | * 13 | * This entity contains all settings of the app that 14 | * affects the media display. 15 | */ 16 | @Stable 17 | @Entity(tableName = "timeline_settings") 18 | data class TimelineSettings( 19 | @PrimaryKey(autoGenerate = true) 20 | val id: Int = 0, 21 | @ColumnInfo(defaultValue = "0") 22 | val groupTimelineByMonth: Boolean = false, 23 | @ColumnInfo(defaultValue = "0") 24 | val groupTimelineInAlbums: Boolean = false, 25 | @ColumnInfo(defaultValue = "{\"orderType\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"},\"orderType_date\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"}}") 26 | val timelineMediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending), 27 | @ColumnInfo(defaultValue = "{\"orderType\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"},\"orderType_date\":{\"type\":\"com.dot.gallery.feature_node.domain.util.OrderType.Descending\"}}") 28 | val albumMediaOrder: MediaOrder = MediaOrder.Date(OrderType.Descending), 29 | ) 30 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/Vault.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | import android.os.Parcelable 4 | import androidx.room.Entity 5 | import kotlinx.parcelize.Parcelize 6 | import kotlinx.serialization.KSerializer 7 | import kotlinx.serialization.Serializable 8 | import kotlinx.serialization.descriptors.PrimitiveKind 9 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 10 | import kotlinx.serialization.descriptors.SerialDescriptor 11 | import kotlinx.serialization.encoding.Decoder 12 | import kotlinx.serialization.encoding.Encoder 13 | import java.util.UUID 14 | import java.io.Serializable as JavaSerializable 15 | 16 | @Parcelize 17 | @Serializable 18 | @Entity(tableName = "vaults", primaryKeys = ["uuid"]) 19 | data class Vault( 20 | @Serializable(with = UUIDSerializer::class) 21 | val uuid: UUID = UUID.randomUUID(), 22 | val name: String 23 | ): Parcelable, JavaSerializable 24 | 25 | object UUIDSerializer : KSerializer { 26 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) 27 | 28 | override fun serialize(encoder: Encoder, value: UUID) { 29 | encoder.encodeString(value.toString()) 30 | } 31 | 32 | override fun deserialize(decoder: Decoder): UUID { 33 | return UUID.fromString(decoder.decodeString()) 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/VaultState.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model 2 | 3 | import androidx.compose.runtime.Stable 4 | import com.dot.gallery.feature_node.presentation.vault.VaultScreens 5 | 6 | @Stable 7 | data class VaultState( 8 | val vaults: List = emptyList(), 9 | val isLoading: Boolean = true 10 | ) { 11 | 12 | fun getStartScreen(): String { 13 | return (if (isLoading) VaultScreens.LoadingScreen else if (vaults.isEmpty()) { 14 | VaultScreens.VaultSetup 15 | } else { 16 | VaultScreens.VaultDisplay 17 | }).invoke() 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/Adjustment.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor 2 | 3 | import android.graphics.Bitmap 4 | import androidx.annotation.Keep 5 | import com.dot.gallery.feature_node.presentation.util.sentenceCase 6 | 7 | @Keep 8 | interface Adjustment { 9 | fun apply(bitmap: Bitmap): Bitmap 10 | 11 | val name: String get() = this::class.simpleName.toString().sentenceCase() 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/CropState.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor 2 | 3 | import android.os.Parcelable 4 | import androidx.compose.runtime.Immutable 5 | import kotlinx.parcelize.Parcelize 6 | import kotlinx.serialization.Serializable 7 | 8 | @Immutable 9 | @Serializable 10 | @Parcelize 11 | data class CropState( 12 | val isCropping: Boolean = false, 13 | val showCropper: Boolean = false 14 | ) : Parcelable -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/CropperAction.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor; 2 | 3 | import androidx.annotation.Keep; 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.automirrored.outlined.RotateRight 6 | import androidx.compose.material.icons.outlined.Crop 7 | import androidx.compose.material.icons.outlined.Flip 8 | import androidx.compose.ui.graphics.vector.ImageVector; 9 | import com.dot.gallery.feature_node.presentation.edit.adjustments.Flip 10 | import com.dot.gallery.feature_node.presentation.edit.adjustments.Rotate90CW 11 | 12 | @Keep 13 | enum class CropperAction { 14 | ROTATE_90, APPLY_CROP, FLIP; 15 | 16 | val icon: ImageVector 17 | get() = when (this) { 18 | APPLY_CROP -> Icons.Outlined.Crop 19 | ROTATE_90 -> Icons.AutoMirrored.Outlined.RotateRight 20 | FLIP -> Icons.Outlined.Flip 21 | } 22 | 23 | 24 | fun asAdjustment(): Adjustment? { 25 | return when (this) { 26 | ROTATE_90 -> Rotate90CW(90f) 27 | FLIP -> Flip(horizontal = true) 28 | else -> null 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/DrawMode.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor 2 | 3 | enum class DrawMode { 4 | Draw, Touch, Erase 5 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/DrawType.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor 2 | 3 | enum class DrawType { 4 | Stylus, Highlighter, Marker 5 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/EditorDestination.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor 2 | 3 | import com.dot.gallery.feature_node.presentation.edit.adjustments.varfilter.VariableFilterTypes 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | sealed class EditorDestination { 8 | 9 | @Serializable 10 | data object Editor : EditorDestination() 11 | 12 | @Serializable 13 | data object Crop : EditorDestination() 14 | 15 | @Serializable 16 | data object Adjust : EditorDestination() 17 | 18 | @Serializable 19 | data class AdjustDetail(val adjustment: VariableFilterTypes) : EditorDestination() 20 | 21 | @Serializable 22 | data object Filters : EditorDestination() 23 | 24 | @Serializable 25 | data object Markup : EditorDestination() 26 | 27 | @Serializable 28 | data object ExternalEditor : EditorDestination() 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/EditorItems.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor; 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.Keep 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.outlined.Adjust 7 | import androidx.compose.material.icons.outlined.Crop 8 | import androidx.compose.material.icons.outlined.Draw 9 | import androidx.compose.material.icons.outlined.Filter 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.graphics.vector.ImageVector 12 | import androidx.compose.ui.res.stringResource 13 | import com.dot.gallery.R 14 | import kotlinx.parcelize.IgnoredOnParcel 15 | import kotlinx.parcelize.Parcelize 16 | import kotlinx.serialization.Serializable 17 | 18 | @Keep 19 | @Serializable 20 | @Parcelize 21 | enum class EditorItems : Parcelable { 22 | Crop, 23 | Adjust, 24 | Filters, 25 | Markup; 26 | 27 | @get:Composable 28 | val translatedName : String 29 | get() = when (this) { 30 | Crop -> stringResource(R.string.crop) 31 | Adjust -> stringResource(R.string.adjust) 32 | Filters -> stringResource(R.string.filters) 33 | Markup -> stringResource(R.string.markup) 34 | } 35 | 36 | @IgnoredOnParcel 37 | val icon: ImageVector 38 | get() = when (this) { 39 | Crop -> Icons.Outlined.Crop 40 | Adjust -> Icons.Outlined.Adjust 41 | Filters -> Icons.Outlined.Filter 42 | Markup -> Icons.Outlined.Draw 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/ImageFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor 2 | 3 | import androidx.annotation.Keep 4 | import androidx.compose.ui.graphics.ColorMatrix 5 | 6 | @Keep 7 | interface ImageFilter : Adjustment { 8 | 9 | fun colorMatrix(): ColorMatrix? 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/MarkupItems.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor 2 | 3 | import android.os.Parcelable 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.res.stringResource 7 | import com.dot.gallery.R 8 | import com.dot.gallery.ui.core.icons.InkHighlighter 9 | import com.dot.gallery.ui.core.icons.InkMarker 10 | import com.dot.gallery.ui.core.icons.Ink_Eraser 11 | import com.dot.gallery.ui.core.icons.Stylus 12 | import kotlinx.parcelize.IgnoredOnParcel 13 | import kotlinx.parcelize.Parcelize 14 | import kotlinx.serialization.Serializable 15 | import com.dot.gallery.ui.core.Icons as DotIcons 16 | 17 | @Serializable 18 | @Parcelize 19 | enum class MarkupItems : Parcelable { 20 | Stylus, 21 | Highlighter, 22 | Marker, 23 | Eraser; 24 | 25 | @get:Composable 26 | val translatedName get() = when (this) { 27 | Stylus -> stringResource(R.string.type_stylus) 28 | Highlighter -> stringResource(R.string.type_highlighter) 29 | Marker -> stringResource(R.string.type_marker) 30 | Eraser -> stringResource(R.string.type_erase) 31 | } 32 | 33 | @IgnoredOnParcel 34 | val icon: ImageVector 35 | get() = when (this) { 36 | Stylus -> DotIcons.Stylus 37 | Highlighter -> DotIcons.InkHighlighter 38 | Marker -> DotIcons.InkMarker 39 | Eraser -> DotIcons.Ink_Eraser 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/PainterMotionEvent.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor 2 | 3 | enum class PainterMotionEvent { 4 | Idle, Down, Move, Up 5 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/PathProperties.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.StrokeCap 5 | import androidx.compose.ui.graphics.StrokeJoin 6 | 7 | class PathProperties( 8 | var strokeWidth: Float = 20f, 9 | var color: Color = Color.Red, 10 | var alpha: Float = 1f, 11 | var strokeCap: StrokeCap = StrokeCap.Round, 12 | var strokeJoin: StrokeJoin = StrokeJoin.Round, 13 | var eraseMode: Boolean = false 14 | ) { 15 | 16 | fun copy( 17 | strokeWidth: Float = this.strokeWidth, 18 | color: Color = this.color, 19 | alpha: Float = this.alpha, 20 | strokeCap: StrokeCap = this.strokeCap, 21 | strokeJoin: StrokeJoin = this.strokeJoin, 22 | eraseMode: Boolean = this.eraseMode 23 | ) = PathProperties( 24 | strokeWidth, color, alpha, strokeCap, strokeJoin, eraseMode 25 | ) 26 | 27 | fun copyFrom(properties: PathProperties) { 28 | this.strokeWidth = properties.strokeWidth 29 | this.color = properties.color 30 | this.strokeCap = properties.strokeCap 31 | this.strokeJoin = properties.strokeJoin 32 | this.eraseMode = properties.eraseMode 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/SaveFormat.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor 2 | 3 | import android.graphics.Bitmap.CompressFormat 4 | import androidx.annotation.Keep 5 | 6 | @Keep 7 | sealed interface SaveFormat { 8 | 9 | val format: CompressFormat 10 | val mimeType: String 11 | 12 | data object PNG : SaveFormat { 13 | override val format = CompressFormat.PNG 14 | override val mimeType = "image/png" 15 | } 16 | 17 | data object JPEG : SaveFormat { 18 | override val format = CompressFormat.JPEG 19 | override val mimeType = "image/jpeg" 20 | } 21 | 22 | data object WEBP_LOSSLESS : SaveFormat { 23 | override val format = CompressFormat.WEBP_LOSSLESS 24 | override val mimeType = "image/webp" 25 | } 26 | 27 | data object WEBP_LOSSY : SaveFormat { 28 | override val format = CompressFormat.WEBP_LOSSY 29 | override val mimeType = "image/webp" 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/model/editor/VariableFilter.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.model.editor 2 | 3 | import android.graphics.Bitmap 4 | import androidx.annotation.Keep 5 | import androidx.compose.ui.graphics.ColorMatrix 6 | 7 | @Keep 8 | interface VariableFilter : Adjustment { 9 | 10 | val maxValue: Float 11 | val minValue: Float 12 | val defaultValue: Float 13 | val value: Float 14 | 15 | fun revert(bitmap: Bitmap): Bitmap 16 | 17 | fun colorMatrix(): ColorMatrix? 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/util/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.util 2 | 3 | import android.net.Uri 4 | import androidx.room.TypeConverter 5 | import com.dot.gallery.feature_node.domain.model.Media 6 | import kotlinx.serialization.encodeToString 7 | import kotlinx.serialization.json.Json 8 | import java.util.UUID 9 | 10 | object Converters { 11 | @TypeConverter 12 | fun toString(value: String?): List = Json.decodeFromString(value ?: "[]") 13 | 14 | @TypeConverter 15 | fun fromList(list: List?): String = Json.encodeToString(list ?: emptyList()) 16 | 17 | @TypeConverter 18 | fun toUri(value: String): Uri = Uri.parse(value) 19 | 20 | @TypeConverter 21 | fun fromUri(uri: Uri): String = uri.toString() 22 | 23 | @TypeConverter 24 | fun toMediaOrder(value: String): MediaOrder = Json.decodeFromString(value) 25 | 26 | @TypeConverter 27 | fun fromMediaOrder(mediaOrder: MediaOrder): String = Json.encodeToString(mediaOrder) 28 | 29 | @TypeConverter 30 | fun fromMedia(media: Media): String = Json.encodeToString(media) 31 | 32 | @TypeConverter 33 | fun toMedia(value: String): Media = Json.decodeFromString(value) 34 | 35 | @TypeConverter 36 | fun fromUUID(uuid: UUID): String = uuid.toString() 37 | 38 | @TypeConverter 39 | fun toUUID(value: String): UUID = UUID.fromString(value) 40 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/util/OrderType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.domain.util 7 | 8 | import android.os.Parcelable 9 | import kotlinx.parcelize.Parcelize 10 | import kotlinx.serialization.Serializable 11 | 12 | @Serializable 13 | @Parcelize 14 | sealed class OrderType : Parcelable { 15 | @Serializable 16 | @Parcelize 17 | data object Ascending : OrderType() 18 | 19 | @Serializable 20 | @Parcelize 21 | data object Descending : OrderType() 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/domain/util/UriSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.domain.util 2 | 3 | import android.net.Uri 4 | import androidx.core.net.toUri 5 | import kotlinx.serialization.KSerializer 6 | import kotlinx.serialization.descriptors.PrimitiveKind 7 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 8 | import kotlinx.serialization.descriptors.SerialDescriptor 9 | import kotlinx.serialization.encoding.Decoder 10 | import kotlinx.serialization.encoding.Encoder 11 | 12 | object UriSerializer : KSerializer { 13 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) 14 | 15 | override fun serialize(encoder: Encoder, value: Uri) { 16 | encoder.encodeString(value.toString()) 17 | } 18 | 19 | override fun deserialize(decoder: Decoder): Uri { 20 | return decoder.decodeString().toUri() 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/MetadataCollectionStatus.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.common.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.LinearProgressIndicator 7 | import androidx.compose.material3.ListItem 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.State 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.unit.dp 14 | import com.dot.gallery.R 15 | import com.dot.gallery.core.Constants.Animation.enterAnimation 16 | import com.dot.gallery.core.Constants.Animation.exitAnimation 17 | import com.dot.gallery.feature_node.domain.model.MediaMetadataState 18 | 19 | @Composable 20 | fun MetadataCollectionStatus( 21 | modifier: Modifier = Modifier, 22 | state: State, 23 | ) { 24 | AnimatedVisibility( 25 | modifier = modifier, 26 | visible = state.value.isLoading && state.value.isLoadingProgress > 1, 27 | enter = enterAnimation, 28 | exit = exitAnimation 29 | ) { 30 | ListItem( 31 | modifier = Modifier.padding( 32 | horizontal = 16.dp, 33 | vertical = 8.dp 34 | ), 35 | headlineContent = { 36 | Text(stringResource(R.string.collecting_metadata)) 37 | }, 38 | supportingContent = { 39 | LinearProgressIndicator( 40 | modifier = Modifier.fillMaxWidth().padding(top = 16.dp), 41 | progress = { 42 | state.value.isLoadingProgress.toFloat() / 100f 43 | }, 44 | ) 45 | }, 46 | ) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/common/components/TwoLinedDateToolbarTitle.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.presentation.common.components 7 | 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.text.font.FontFamily 14 | import androidx.compose.ui.text.style.TextOverflow 15 | 16 | @Composable 17 | fun TwoLinedDateToolbarTitle( 18 | albumName: String, 19 | dateHeader: String = "" 20 | ) { 21 | Column { 22 | Text( 23 | text = albumName, 24 | overflow = TextOverflow.Ellipsis, 25 | maxLines = 1 26 | ) 27 | if (dateHeader.isNotEmpty()) { 28 | Text( 29 | modifier = Modifier, 30 | text = dateHeader.uppercase(), 31 | style = MaterialTheme.typography.labelSmall, 32 | fontFamily = FontFamily.Monospace, 33 | overflow = TextOverflow.Ellipsis, 34 | maxLines = 1 35 | ) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/Crop.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments 2 | 3 | import android.graphics.Bitmap 4 | import com.dot.gallery.feature_node.domain.model.editor.Adjustment 5 | 6 | data class Crop(val newBitmap: Bitmap): Adjustment { 7 | 8 | override fun apply(bitmap: Bitmap): Bitmap { 9 | return newBitmap 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/Flip.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments 2 | 3 | import android.graphics.Bitmap 4 | import com.dot.gallery.feature_node.domain.model.editor.Adjustment 5 | import com.dot.gallery.feature_node.presentation.util.flipHorizontally 6 | import com.dot.gallery.feature_node.presentation.util.flipVertically 7 | 8 | data class Flip( 9 | val horizontal: Boolean, 10 | ) : Adjustment { 11 | 12 | override fun apply(bitmap: Bitmap): Bitmap { 13 | return if (horizontal) bitmap.flipHorizontally() else bitmap.flipVertically() 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/Markup.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments 2 | 3 | import android.graphics.Bitmap 4 | import com.dot.gallery.feature_node.domain.model.editor.Adjustment 5 | 6 | data class Markup(val newBitmap: Bitmap): Adjustment { 7 | 8 | override fun apply(bitmap: Bitmap): Bitmap { 9 | return newBitmap 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/Rotate90CW.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments 2 | 3 | import android.graphics.Bitmap 4 | import com.dot.gallery.feature_node.domain.model.editor.Adjustment 5 | import com.dot.gallery.feature_node.presentation.util.rotate 6 | 7 | data class Rotate90CW( 8 | val angle: Float 9 | ) : Adjustment { 10 | 11 | override fun apply(bitmap: Bitmap): Bitmap { 12 | return bitmap.rotate(angle) 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/filters/Cool.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.filters 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.ui.graphics.ColorMatrix 5 | import com.awxkee.aire.Aire 6 | import com.dot.gallery.feature_node.domain.model.editor.ImageFilter 7 | import com.dot.gallery.feature_node.presentation.util.to3x3Matrix 8 | 9 | data class Cool(override val name: String = "Cool") : ImageFilter { 10 | 11 | override fun colorMatrix(): ColorMatrix = ColorMatrix( 12 | floatArrayOf( 13 | 1f, 0f, 0f, 0f, 0f, 14 | 0f, 1f, 0f, 0f, 0f, 15 | 0f, 0f, 1.2f, 0f, 0f, 16 | 0f, 0f, 0f, 1f, 0f 17 | ) 18 | ) 19 | 20 | override fun apply(bitmap: Bitmap): Bitmap = 21 | Aire.colorMatrix(bitmap, colorMatrix().values.to3x3Matrix()) 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/filters/ImageFilterTypes.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.filters 2 | 3 | import androidx.annotation.Keep 4 | import com.dot.gallery.feature_node.domain.model.editor.ImageFilter 5 | import kotlinx.serialization.Serializable 6 | 7 | @Keep 8 | @Serializable 9 | enum class ImageFilterTypes { 10 | None, Monochrome, Negative, Cool, Warm, Sepia, Posterize, Vintage; 11 | 12 | fun createImageFilter(): ImageFilter = 13 | when (this) { 14 | Monochrome -> Monochrome() 15 | Negative -> Negative() 16 | Cool -> Cool() 17 | Warm -> Warm() 18 | Sepia -> Sepia() 19 | Posterize -> Posterize() 20 | Vintage -> Vintage() 21 | None -> None() 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/filters/Monochrome.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.filters 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.ui.graphics.ColorMatrix 5 | import com.awxkee.aire.Aire 6 | import com.dot.gallery.feature_node.domain.model.editor.ImageFilter 7 | 8 | data class Monochrome(override val name: String = "Monochrome") : ImageFilter { 9 | 10 | override fun colorMatrix(): ColorMatrix = ColorMatrix( 11 | floatArrayOf( 12 | 0.33f, 0.33f, 0.33f, 0f, 0f, 13 | 0.33f, 0.33f, 0.33f, 0f, 0f, 14 | 0.33f, 0.33f, 0.33f, 0f, 0f, 15 | 0f, 0f, 0f, 1f, 0f 16 | ) 17 | ) 18 | 19 | override fun apply(bitmap: Bitmap): Bitmap = 20 | Aire.monochrome(bitmap, floatArrayOf(0.33f, 0.33f, 0.33f, 1f)) 21 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/filters/Negative.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.filters 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import androidx.compose.ui.graphics.ColorMatrix 7 | import androidx.compose.ui.graphics.ColorMatrixColorFilter 8 | import androidx.compose.ui.graphics.asAndroidColorFilter 9 | import com.dot.gallery.feature_node.domain.model.editor.ImageFilter 10 | 11 | data class Negative(override val name: String = "Negative") : ImageFilter { 12 | 13 | override fun colorMatrix(): ColorMatrix = ColorMatrix( 14 | floatArrayOf( 15 | -1f, 0f, 0f, 0f, 255f, 16 | 0f, -1f, 0f, 0f, 255f, 17 | 0f, 0f, -1f, 0f, 255f, 18 | 0f, 0f, 0f, 1f, 0f 19 | ) 20 | ) 21 | 22 | override fun apply(bitmap: Bitmap): Bitmap { 23 | val resultBitmap = Bitmap.createBitmap( 24 | bitmap.width, 25 | bitmap.height, 26 | bitmap.config ?: Bitmap.Config.ARGB_8888 27 | ) 28 | val canvas = Canvas(resultBitmap) 29 | val paint = Paint() 30 | paint.colorFilter = ColorMatrixColorFilter(colorMatrix()).asAndroidColorFilter() 31 | canvas.drawBitmap(bitmap, 0f, 0f, paint) 32 | return resultBitmap 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/filters/None.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.filters 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.ui.graphics.ColorMatrix 5 | import com.dot.gallery.feature_node.domain.model.editor.ImageFilter 6 | 7 | data class None(override val name: String = "None") : ImageFilter { 8 | 9 | override fun colorMatrix(): ColorMatrix? = null 10 | 11 | override fun apply(bitmap: Bitmap): Bitmap = bitmap 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/filters/Posterize.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.filters 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.ui.graphics.ColorMatrix 5 | import com.awxkee.aire.Aire 6 | import com.dot.gallery.feature_node.domain.model.editor.ImageFilter 7 | import com.dot.gallery.feature_node.presentation.util.to3x3Matrix 8 | 9 | data class Posterize(override val name: String = "Posterize") : ImageFilter { 10 | 11 | override fun colorMatrix(): ColorMatrix = ColorMatrix( 12 | floatArrayOf( 13 | 0.5f, 0f, 0f, 0f, 0f, 14 | 0f, 0.5f, 0f, 0f, 0f, 15 | 0f, 0f, 0.5f, 0f, 0f, 16 | 0f, 0f, 0f, 1f, 0f 17 | ) 18 | ) 19 | 20 | override fun apply(bitmap: Bitmap): Bitmap = 21 | Aire.colorMatrix(bitmap, colorMatrix().values.to3x3Matrix()) 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/filters/Sepia.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.filters 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.ui.graphics.ColorMatrix 5 | import com.awxkee.aire.Aire 6 | import com.dot.gallery.feature_node.domain.model.editor.ImageFilter 7 | import com.dot.gallery.feature_node.presentation.util.to3x3Matrix 8 | 9 | data class Sepia(override val name: String = "Sepia") : ImageFilter { 10 | 11 | override fun colorMatrix(): ColorMatrix = ColorMatrix( 12 | floatArrayOf( 13 | 0.393f, 0.769f, 0.189f, 0f, 0f, 14 | 0.349f, 0.686f, 0.168f, 0f, 0f, 15 | 0.272f, 0.534f, 0.131f, 0f, 0f, 16 | 0f, 0f, 0f, 1f, 0f 17 | ) 18 | ) 19 | 20 | override fun apply(bitmap: Bitmap): Bitmap = 21 | Aire.colorMatrix(bitmap, colorMatrix().values.to3x3Matrix()) 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/filters/Vintage.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.filters 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.ui.graphics.ColorMatrix 5 | import com.awxkee.aire.Aire 6 | import com.dot.gallery.feature_node.domain.model.editor.ImageFilter 7 | import com.dot.gallery.feature_node.presentation.util.to3x3Matrix 8 | 9 | data class Vintage(override val name: String = "Vintage") : ImageFilter { 10 | 11 | override fun colorMatrix(): ColorMatrix = ColorMatrix( 12 | floatArrayOf( 13 | 0.9f, 0.5f, 0.1f, 0f, 0f, 14 | 0.3f, 0.7f, 0.2f, 0f, 0f, 15 | 0.2f, 0.3f, 0.4f, 0f, 0f, 16 | 0f, 0f, 0f, 1f, 0f 17 | ) 18 | ) 19 | 20 | override fun apply(bitmap: Bitmap): Bitmap = 21 | Aire.colorMatrix(bitmap, colorMatrix().values.to3x3Matrix()) 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/filters/Warm.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.filters 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.ui.graphics.ColorMatrix 5 | import com.awxkee.aire.Aire 6 | import com.dot.gallery.feature_node.domain.model.editor.ImageFilter 7 | import com.dot.gallery.feature_node.presentation.util.to3x3Matrix 8 | 9 | data class Warm(override val name: String = "Warm") : ImageFilter { 10 | 11 | override fun colorMatrix(): ColorMatrix = ColorMatrix( 12 | floatArrayOf( 13 | 1.2f, 0f, 0f, 0f, 0f, 14 | 0f, 1f, 0f, 0f, 0f, 15 | 0f, 0f, 0.8f, 0f, 0f, 16 | 0f, 0f, 0f, 1f, 0f 17 | ) 18 | ) 19 | 20 | override fun apply(bitmap: Bitmap): Bitmap = 21 | Aire.colorMatrix(bitmap, colorMatrix().values.to3x3Matrix()) 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/varfilter/Brightness.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.varfilter 2 | 3 | import android.graphics.Bitmap 4 | import androidx.annotation.FloatRange 5 | import androidx.compose.ui.graphics.ColorMatrix 6 | import com.awxkee.aire.Aire 7 | import com.dot.gallery.feature_node.domain.model.editor.VariableFilter 8 | 9 | data class Brightness( 10 | @FloatRange(from = -1.0, to = 1.0) 11 | override val value: Float = 0f 12 | ) : VariableFilter { 13 | override val maxValue = 1f 14 | override val minValue = -1f 15 | override val defaultValue = 0f 16 | 17 | override fun apply(bitmap: Bitmap): Bitmap { 18 | return Aire.brightness(bitmap, value) 19 | } 20 | 21 | override fun revert(bitmap: Bitmap): Bitmap { 22 | return Aire.brightness(bitmap, -value) 23 | } 24 | 25 | override fun colorMatrix(): ColorMatrix = 26 | ColorMatrix( 27 | floatArrayOf( 28 | 1f, 0f, 0f, 0f, value * 255, 29 | 0f, 1f, 0f, 0f, value * 255, 30 | 0f, 0f, 1f, 0f, value * 255, 31 | 0f, 0f, 0f, 1f, 0f 32 | ) 33 | ) 34 | } 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/varfilter/Contrast.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.varfilter 2 | 3 | import android.graphics.Bitmap 4 | import androidx.annotation.FloatRange 5 | import androidx.compose.ui.graphics.ColorMatrix 6 | import com.awxkee.aire.Aire 7 | import com.dot.gallery.feature_node.domain.model.editor.VariableFilter 8 | 9 | data class Contrast( 10 | @FloatRange(from = 0.0, to = 2.0) 11 | override val value: Float = 1.0f 12 | ) : VariableFilter { 13 | override val maxValue = 2f 14 | override val minValue = 0f 15 | override val defaultValue = 1f 16 | 17 | override fun apply(bitmap: Bitmap): Bitmap { 18 | return Aire.contrast(bitmap, value) 19 | } 20 | 21 | override fun revert(bitmap: Bitmap): Bitmap { 22 | return Aire.contrast(bitmap, -value) 23 | } 24 | 25 | override fun colorMatrix(): ColorMatrix = 26 | ColorMatrix( 27 | floatArrayOf( 28 | value, 0f, 0f, 0f, 128f * (1 - value), 29 | 0f, value, 0f, 0f, 128f * (1 - value), 30 | 0f, 0f, value, 0f, 128f * (1 - value), 31 | 0f, 0f, 0f, 1f, 0f 32 | ) 33 | ) 34 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/varfilter/Rotate.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.varfilter 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.ui.graphics.ColorMatrix 5 | import com.dot.gallery.feature_node.domain.model.editor.VariableFilter 6 | import com.dot.gallery.feature_node.presentation.util.rotate 7 | 8 | data class Rotate( 9 | override val value: Float = 0f 10 | ) : VariableFilter { 11 | 12 | override val maxValue = 180f 13 | override val minValue = -180f 14 | override val defaultValue = 0f 15 | 16 | override fun revert(bitmap: Bitmap): Bitmap = bitmap.rotate(-value) 17 | 18 | override fun colorMatrix(): ColorMatrix? = null 19 | 20 | override fun apply(bitmap: Bitmap): Bitmap = bitmap.rotate(value) 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/varfilter/Saturation.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.varfilter 2 | 3 | import android.graphics.Bitmap 4 | import androidx.annotation.FloatRange 5 | import androidx.compose.ui.graphics.ColorMatrix 6 | import com.awxkee.aire.Aire 7 | import com.dot.gallery.feature_node.domain.model.editor.VariableFilter 8 | 9 | data class Saturation( 10 | @FloatRange(from = 0.0, to = 2.0) 11 | override val value: Float = 1f 12 | ) : VariableFilter { 13 | override val maxValue = 2f 14 | override val minValue = 0f 15 | override val defaultValue = 1f 16 | 17 | override fun apply(bitmap: Bitmap): Bitmap { 18 | return Aire.saturation(bitmap, value, tonemap = false) 19 | } 20 | 21 | override fun revert(bitmap: Bitmap): Bitmap { 22 | return Aire.saturation(bitmap, -value, tonemap = false) 23 | } 24 | 25 | override fun colorMatrix(): ColorMatrix = 26 | ColorMatrix( 27 | floatArrayOf( 28 | 0.213f * (1 - value) + value, 0.715f * (1 - value), 0.072f * (1 - value), 0f, 0f, 29 | 0.213f * (1 - value), 0.715f * (1 - value) + value, 0.072f * (1 - value), 0f, 0f, 30 | 0.213f * (1 - value), 0.715f * (1 - value), 0.072f * (1 - value) + value, 0f, 0f, 31 | 0f, 0f, 0f, 1f, 0f 32 | ) 33 | ) 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/varfilter/Sharpness.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.varfilter 2 | 3 | import android.graphics.Bitmap 4 | import androidx.annotation.FloatRange 5 | import androidx.compose.ui.graphics.ColorMatrix 6 | import com.awxkee.aire.Aire 7 | import com.dot.gallery.feature_node.domain.model.editor.VariableFilter 8 | 9 | data class Sharpness( 10 | @FloatRange(from = -1.0, to = 10.0) 11 | override val value: Float = 0f 12 | ) : VariableFilter { 13 | override val maxValue = 10f 14 | override val minValue = -1f 15 | override val defaultValue = 0f 16 | 17 | override fun apply(bitmap: Bitmap): Bitmap { 18 | return Aire.sharpness(bitmap, value) 19 | } 20 | 21 | override fun revert(bitmap: Bitmap): Bitmap { 22 | return Aire.sharpness(bitmap, -value) 23 | } 24 | 25 | override fun colorMatrix(): ColorMatrix? = null 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/adjustments/varfilter/VariableFilterTypes.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.adjustments.varfilter 2 | 3 | import androidx.annotation.Keep 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.outlined.Brightness4 6 | import androidx.compose.material.icons.outlined.Contrast 7 | import androidx.compose.material.icons.outlined.InvertColors 8 | import androidx.compose.material.icons.outlined.Rotate90DegreesCcw 9 | import androidx.compose.ui.graphics.vector.ImageVector 10 | import com.dot.gallery.feature_node.domain.model.editor.VariableFilter 11 | import kotlinx.serialization.Serializable 12 | 13 | @Keep 14 | @Serializable 15 | enum class VariableFilterTypes { 16 | Brightness, Contrast, Saturation, Rotate/*, Sharpness*/; 17 | 18 | fun createFilter(value: Float): VariableFilter = 19 | when (this) { 20 | Contrast -> Contrast(value) 21 | Saturation -> Saturation(value) 22 | //Sharpness -> Sharpness(value) 23 | Brightness -> Brightness(value) 24 | Rotate -> Rotate(value) 25 | } 26 | 27 | fun createDefaultFilter(): VariableFilter = 28 | when (this) { 29 | Contrast -> Contrast() 30 | Saturation -> Saturation() 31 | //Sharpness -> Sharpness() 32 | Brightness -> Brightness() 33 | Rotate -> Rotate() 34 | } 35 | 36 | val icon: ImageVector get() = 37 | when (this) { 38 | Brightness -> Icons.Outlined.Brightness4 39 | Contrast -> Icons.Outlined.Contrast 40 | Saturation -> Icons.Outlined.InvertColors 41 | Rotate -> Icons.Outlined.Rotate90DegreesCcw 42 | //Sharpness -> Icons.Outlined.Details 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/components/core/SupportiveLayout.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.components.core 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun SupportiveLayout( 15 | modifier: Modifier = Modifier, 16 | isSupportingPanel: Boolean, 17 | content: @Composable () -> Unit 18 | ) { 19 | if (isSupportingPanel) { 20 | Row( 21 | modifier = modifier 22 | .animateContentSize() 23 | .fillMaxWidth(), 24 | verticalAlignment = Alignment.CenterVertically, 25 | horizontalArrangement = Arrangement.spacedBy(8.dp), 26 | content = { 27 | content() 28 | } 29 | ) 30 | } else { 31 | Column( 32 | modifier = modifier 33 | .animateContentSize() 34 | .fillMaxWidth(), 35 | horizontalAlignment = Alignment.CenterHorizontally, 36 | verticalArrangement = Arrangement.spacedBy(8.dp), 37 | content = { 38 | content() 39 | } 40 | ) 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/components/core/SupportiveLazyGridLayout.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.components.core 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.lazy.LazyListScope 9 | import androidx.compose.foundation.lazy.LazyRow 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun SupportiveLazyGridLayout( 16 | modifier: Modifier = Modifier, 17 | isSupportingPanel: Boolean, 18 | contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp), 19 | content: LazyListScope.() -> Unit 20 | ) { 21 | if (!isSupportingPanel) { 22 | LazyRow( 23 | modifier = modifier 24 | .animateContentSize() 25 | .fillMaxWidth(), 26 | horizontalArrangement = Arrangement.SpaceBetween, 27 | contentPadding = contentPadding, 28 | content = content 29 | ) 30 | } else { 31 | LazyColumn( 32 | modifier = modifier 33 | .animateContentSize() 34 | .fillMaxWidth(), 35 | verticalArrangement = Arrangement.SpaceBetween, 36 | contentPadding = contentPadding, 37 | content = content 38 | ) 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/components/core/SupportiveLazyLayout.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.components.core 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.lazy.LazyListScope 9 | import androidx.compose.foundation.lazy.LazyRow 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun SupportiveLazyLayout( 16 | modifier: Modifier = Modifier, 17 | isSupportingPanel: Boolean, 18 | contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp), 19 | content: LazyListScope.() -> Unit 20 | ) { 21 | if (!isSupportingPanel) { 22 | LazyRow( 23 | modifier = modifier 24 | .animateContentSize() 25 | .fillMaxWidth(), 26 | horizontalArrangement = Arrangement.SpaceBetween, 27 | contentPadding = contentPadding, 28 | content = content 29 | ) 30 | } else { 31 | LazyColumn( 32 | modifier = modifier 33 | .animateContentSize() 34 | .fillMaxWidth(), 35 | verticalArrangement = Arrangement.SpaceBetween, 36 | contentPadding = contentPadding, 37 | content = content 38 | ) 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/components/cropper/CropperSection.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.components.cropper 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.lazy.items 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.draw.clipToBounds 13 | import androidx.compose.ui.unit.dp 14 | import com.dot.gallery.feature_node.domain.model.editor.CropperAction 15 | import com.dot.gallery.feature_node.presentation.edit.components.adjustment.SelectableItem 16 | import com.dot.gallery.feature_node.presentation.edit.components.core.SupportiveLazyLayout 17 | 18 | @Composable 19 | fun CropperSection( 20 | modifier: Modifier = Modifier, 21 | isSupportingPanel: Boolean, 22 | onActionClick: (CropperAction) -> Unit = {} 23 | ) { 24 | val actions = remember { 25 | CropperAction.entries.toList() 26 | } 27 | 28 | val padding = remember(isSupportingPanel) { 29 | if (isSupportingPanel) PaddingValues(0.dp) else PaddingValues(16.dp) 30 | } 31 | 32 | SupportiveLazyLayout( 33 | modifier = modifier 34 | .animateContentSize() 35 | .fillMaxWidth() 36 | .then( 37 | if (isSupportingPanel) Modifier 38 | .clipToBounds() 39 | .clip(RoundedCornerShape(16.dp)) 40 | else Modifier 41 | ), 42 | contentPadding = padding, 43 | isSupportingPanel = isSupportingPanel 44 | ) { 45 | items( 46 | items = actions, 47 | key = { it.name } 48 | ) { item -> 49 | SelectableItem( 50 | icon = item.icon, 51 | title = item.name, 52 | selected = item == CropperAction.APPLY_CROP, 53 | onItemClick = { onActionClick(item) }, 54 | horizontal = isSupportingPanel 55 | ) 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/components/editor/EditorSelector.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.components.editor 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.foundation.lazy.itemsIndexed 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.draw.clipToBounds 13 | import androidx.compose.ui.unit.dp 14 | import com.dot.gallery.feature_node.domain.model.editor.EditorItems 15 | import com.dot.gallery.feature_node.presentation.edit.components.core.SupportiveLazyLayout 16 | import com.dot.gallery.feature_node.presentation.util.safeSystemGesturesPadding 17 | 18 | @Composable 19 | fun EditorSelector( 20 | modifier: Modifier = Modifier, 21 | isSupportingPanel: Boolean, 22 | onItemClick: (EditorItems) -> Unit = {} 23 | ) { 24 | 25 | val padding = remember(isSupportingPanel) { 26 | if (isSupportingPanel) PaddingValues(0.dp) else PaddingValues(horizontal = 16.dp, vertical = 2.dp) 27 | } 28 | 29 | SupportiveLazyLayout( 30 | modifier = modifier 31 | .then( 32 | if (isSupportingPanel) Modifier 33 | .safeSystemGesturesPadding(onlyRight = true) 34 | .clipToBounds() 35 | .clip(RoundedCornerShape(16.dp)) 36 | else Modifier 37 | ), 38 | isSupportingPanel = isSupportingPanel, 39 | contentPadding = padding 40 | ) { 41 | itemsIndexed( 42 | items = EditorItems.entries, 43 | key = { _, it -> it.name } 44 | ) { index, editorItem -> 45 | EditorItem( 46 | imageVector = editorItem.icon, 47 | title = editorItem.translatedName, 48 | horizontal = isSupportingPanel, 49 | onItemClick = { 50 | onItemClick(editorItem) 51 | } 52 | ) 53 | if (isSupportingPanel && index < EditorItems.entries.size - 1) { 54 | Spacer(modifier = Modifier.size(16.dp)) 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/components/markup/MarkupPathPreview.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.components.markup 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Path 9 | import androidx.compose.ui.graphics.drawscope.Stroke 10 | import androidx.compose.ui.unit.dp 11 | import com.dot.gallery.feature_node.domain.model.editor.PathProperties 12 | 13 | @Composable 14 | fun MarkupPathPreview( 15 | modifier: Modifier = Modifier, 16 | pathProperties: PathProperties, 17 | isSupportingPanel: Boolean 18 | ) { 19 | Canvas( 20 | modifier = modifier.padding(16.dp).size(40.dp) 21 | ) { 22 | val path = Path() 23 | if (isSupportingPanel) { 24 | path.moveTo(size.width / 2, 0f) 25 | path.lineTo(size.width / 2, size.height) 26 | } else { 27 | path.moveTo(0f, size.height / 2) 28 | path.lineTo(size.width, size.height / 2) 29 | } 30 | 31 | drawPath( 32 | color = pathProperties.color, 33 | path = path, 34 | style = Stroke( 35 | width = pathProperties.strokeWidth, 36 | cap = pathProperties.strokeCap, 37 | join = pathProperties.strokeJoin 38 | ) 39 | ) 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/edit/utils/isApplied.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.edit.utils 2 | 3 | import androidx.annotation.Keep 4 | import com.dot.gallery.feature_node.domain.model.editor.Adjustment 5 | import com.dot.gallery.feature_node.presentation.edit.adjustments.varfilter.VariableFilterTypes 6 | 7 | @Keep 8 | fun List.isApplied(variableFilterTypes: VariableFilterTypes): Boolean { 9 | return any { it.name == variableFilterTypes.name } 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/favorites/components/EmptyFavorites.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.presentation.favorites.components 7 | 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.material.icons.Icons 13 | import androidx.compose.material.icons.outlined.FavoriteBorder 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.dp 22 | import com.dot.gallery.R 23 | 24 | @Composable 25 | fun EmptyFavorites( 26 | modifier: Modifier = Modifier, 27 | title: String = stringResource(R.string.empty_favorites_title), 28 | ) { 29 | Column( 30 | modifier = modifier.padding(top = 64.dp), 31 | horizontalAlignment = Alignment.CenterHorizontally, 32 | verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically) 33 | ) { 34 | Icon( 35 | modifier = Modifier 36 | .size(128.dp), 37 | imageVector = Icons.Outlined.FavoriteBorder, 38 | contentDescription = stringResource(R.string.empty_favorites_title), 39 | tint = MaterialTheme.colorScheme.primary 40 | ) 41 | Text( 42 | text = title, 43 | style = MaterialTheme.typography.titleLarge 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/favorites/components/FavoriteNavActions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.presentation.favorites.components 7 | 8 | import androidx.activity.result.ActivityResultLauncher 9 | import androidx.activity.result.IntentSenderRequest 10 | import androidx.compose.animation.AnimatedVisibility 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TextButton 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.MutableState 16 | import androidx.compose.runtime.State 17 | import androidx.compose.ui.res.stringResource 18 | import com.dot.gallery.R 19 | import com.dot.gallery.core.Constants.Animation.enterAnimation 20 | import com.dot.gallery.core.Constants.Animation.exitAnimation 21 | import com.dot.gallery.feature_node.domain.model.Media 22 | import com.dot.gallery.feature_node.domain.model.MediaState 23 | import com.dot.gallery.feature_node.presentation.util.selectedMedia 24 | 25 | @Composable 26 | fun FavoriteNavActions( 27 | toggleFavorite: (ActivityResultLauncher, List, Boolean) -> Unit, 28 | mediaState: State>, 29 | selectedMedia: MutableState>, 30 | selectionState: MutableState, 31 | result: ActivityResultLauncher 32 | ) { 33 | val selectedMediaList = mediaState.value.media.selectedMedia(selectedSet = selectedMedia) 34 | val removeAllTitle = stringResource(R.string.remove_all) 35 | val removeSelectedTitle = stringResource(R.string.remove_selected) 36 | val title = if (selectionState.value) removeSelectedTitle else removeAllTitle 37 | AnimatedVisibility( 38 | visible = mediaState.value.media.isNotEmpty(), 39 | enter = enterAnimation, 40 | exit = exitAnimation 41 | ) { 42 | TextButton( 43 | onClick = { 44 | toggleFavorite(result, selectedMediaList.ifEmpty { mediaState.value.media }, false) 45 | } 46 | ) { 47 | Text( 48 | text = title, 49 | color = MaterialTheme.colorScheme.primary 50 | ) 51 | } 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/IgnoredState.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.ignored 2 | 3 | import android.os.Parcelable 4 | import androidx.compose.runtime.Immutable 5 | import com.dot.gallery.feature_node.domain.model.IgnoredAlbum 6 | import kotlinx.parcelize.Parcelize 7 | 8 | @Immutable 9 | @Parcelize 10 | data class IgnoredState( 11 | val albums: List = emptyList() 12 | ): Parcelable 13 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/IgnoredViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.ignored 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.dot.gallery.feature_node.domain.model.IgnoredAlbum 6 | import com.dot.gallery.feature_node.domain.repository.MediaRepository 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.flow.SharingStarted 9 | import kotlinx.coroutines.flow.map 10 | import kotlinx.coroutines.flow.stateIn 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class IgnoredViewModel @Inject constructor( 16 | private val repository: MediaRepository 17 | ): ViewModel() { 18 | 19 | val blacklistState = repository.getBlacklistedAlbums() 20 | .map { IgnoredState(it) } 21 | .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), IgnoredState()) 22 | 23 | fun removeFromBlacklist(ignoredAlbum: IgnoredAlbum) { 24 | viewModelScope.launch { 25 | repository.removeBlacklistedAlbum(ignoredAlbum) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetupDestination.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.ignored.setup 2 | 3 | sealed class IgnoredSetupDestination(val route: String) { 4 | data object Label : IgnoredSetupDestination("label") 5 | data object Location : IgnoredSetupDestination("location") 6 | data object Type : IgnoredSetupDestination("type") 7 | data object MatchedAlbums : IgnoredSetupDestination("matched_albums") 8 | 9 | operator fun invoke() = route 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetupStage.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.ignored.setup 2 | 3 | enum class IgnoredSetupStage { 4 | LABEL, 5 | LOCATION, 6 | TYPE, 7 | MATCHED_ALBUMS 8 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetupState.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.ignored.setup 2 | 3 | import com.dot.gallery.feature_node.domain.model.Album 4 | import com.dot.gallery.feature_node.domain.model.IgnoredAlbum 5 | 6 | data class IgnoredSetupState( 7 | val label: String = "", 8 | val location: Int = IgnoredAlbum.ALBUMS_ONLY, 9 | val type: IgnoredType = IgnoredType.SELECTION(null), 10 | val matchedAlbums: List = emptyList(), 11 | val stage: IgnoredSetupStage = IgnoredSetupStage.LABEL 12 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredSetupViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.ignored.setup 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.dot.gallery.feature_node.domain.model.Album 9 | import com.dot.gallery.feature_node.domain.model.IgnoredAlbum 10 | import com.dot.gallery.feature_node.domain.repository.MediaRepository 11 | import com.dot.gallery.feature_node.presentation.ignored.IgnoredState 12 | import dagger.hilt.android.lifecycle.HiltViewModel 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.SharingStarted 15 | import kotlinx.coroutines.flow.asStateFlow 16 | import kotlinx.coroutines.flow.map 17 | import kotlinx.coroutines.flow.stateIn 18 | import kotlinx.coroutines.launch 19 | import javax.inject.Inject 20 | 21 | @HiltViewModel 22 | class IgnoredSetupViewModel @Inject constructor( 23 | private val repository: MediaRepository 24 | ): ViewModel() { 25 | 26 | private val _uiState = MutableStateFlow(IgnoredSetupState()) 27 | val uiState = _uiState.asStateFlow() 28 | 29 | var isLabelError by mutableStateOf(false) 30 | 31 | val blacklistState = repository.getBlacklistedAlbums() 32 | .map { IgnoredState(it) } 33 | .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), IgnoredState()) 34 | 35 | fun setLabel(label: String) { 36 | _uiState.value = _uiState.value.copy(label = label) 37 | isLabelError = label.isEmpty() 38 | } 39 | 40 | fun setLocation(location: Int) { 41 | _uiState.value = _uiState.value.copy(location = location) 42 | } 43 | 44 | fun setType(type: IgnoredType) { 45 | _uiState.value = _uiState.value.copy(type = type) 46 | } 47 | 48 | fun setMatchedAlbums(matchedAlbums: List) { 49 | _uiState.value = _uiState.value.copy(matchedAlbums = matchedAlbums) 50 | } 51 | 52 | fun reset() { 53 | _uiState.value = IgnoredSetupState() 54 | } 55 | 56 | fun addToIgnored(ignoredAlbum: IgnoredAlbum) { 57 | viewModelScope.launch { 58 | repository.addBlacklistedAlbum(ignoredAlbum) 59 | } 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/ignored/setup/IgnoredType.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.ignored.setup 2 | 3 | import com.dot.gallery.feature_node.domain.model.Album 4 | 5 | sealed class IgnoredType { 6 | 7 | data class SELECTION(val selectedAlbum: Album?) : IgnoredType() 8 | 9 | data class REGEX(val regex: String) : IgnoredType() 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/library/LibraryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.library 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import androidx.work.WorkManager 6 | import com.dot.gallery.feature_node.domain.model.LibraryIndicatorState 7 | import com.dot.gallery.feature_node.domain.repository.MediaRepository 8 | import com.dot.gallery.feature_node.domain.util.MediaOrder 9 | import com.dot.gallery.feature_node.presentation.classifier.startClassification 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.flow.SharingStarted 12 | import kotlinx.coroutines.flow.combine 13 | import kotlinx.coroutines.flow.map 14 | import kotlinx.coroutines.flow.stateIn 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class LibraryViewModel @Inject constructor( 19 | repository: MediaRepository, 20 | private val workManager: WorkManager 21 | ) : ViewModel() { 22 | 23 | val indicatorState = combine( 24 | repository.getTrashed(), 25 | repository.getFavorites(MediaOrder.Default) 26 | ) { trashed, favorites -> 27 | LibraryIndicatorState( 28 | trashCount = trashed.data?.size ?: 0, 29 | favoriteCount = favorites.data?.size ?: 0 30 | ) 31 | }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), LibraryIndicatorState()) 32 | 33 | val classifiedCategories = repository.getClassifiedCategories() 34 | .map { if (it.isNotEmpty()) it.distinct() else it } 35 | .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) 36 | 37 | val mostPopularCategory = repository.getClassifiedMediaByMostPopularCategory() 38 | .map { it.groupBy { it.category!! }.toSortedMap() } 39 | .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyMap()) 40 | 41 | fun startClassification() { 42 | workManager.startClassification() 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/video/createDecryptedVideoFile.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.mediaview.components.video 2 | 3 | import androidx.core.net.toFile 4 | import com.dot.gallery.feature_node.data.data_source.KeychainHolder 5 | import com.dot.gallery.feature_node.domain.model.Media 6 | import com.dot.gallery.feature_node.domain.model.Media.EncryptedMedia 7 | import com.dot.gallery.feature_node.domain.util.getUri 8 | import java.io.File 9 | import java.io.FileOutputStream 10 | 11 | fun createDecryptedVideoFile(keychainHolder: KeychainHolder, decryptedMedia: T): File { 12 | // Create a temporary file 13 | val tempFile = File.createTempFile("${decryptedMedia.id}.temp", null) 14 | val encryptedFile = decryptedMedia.getUri().toFile() 15 | val encryptedMedia = with(keychainHolder) { 16 | encryptedFile.decryptKotlin() 17 | } 18 | 19 | // Write the ByteArray to the temporary file 20 | FileOutputStream(tempFile).use { fileOutputStream -> 21 | fileOutputStream.write(encryptedMedia.bytes) 22 | fileOutputStream.flush() 23 | } 24 | 25 | return tempFile 26 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/search/components/SearchBarElevation.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.search.components 2 | 3 | import androidx.compose.ui.unit.Dp 4 | import androidx.compose.ui.unit.dp 5 | 6 | sealed class SearchBarElevation(val dp: Dp) { 7 | 8 | data object Collapsed : SearchBarElevation(2.dp) 9 | 10 | data object Expanded : SearchBarElevation(0.dp) 11 | 12 | operator fun invoke() = dp 13 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/trashed/components/AutoDeleteFooter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.presentation.trashed.components 7 | 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.outlined.Info 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.res.stringResource 22 | import androidx.compose.ui.unit.dp 23 | import com.dot.gallery.R 24 | 25 | @Composable 26 | fun AutoDeleteFooter() { 27 | Row( 28 | modifier = Modifier 29 | .fillMaxWidth() 30 | .padding(16.dp) 31 | .padding(vertical = 16.dp), 32 | verticalAlignment = Alignment.CenterVertically, 33 | horizontalArrangement = Arrangement.spacedBy(16.dp) 34 | ) { 35 | Icon( 36 | modifier = Modifier.size(24.dp), 37 | imageVector = Icons.Outlined.Info, 38 | contentDescription = null, 39 | tint = MaterialTheme.colorScheme.onSurfaceVariant 40 | ) 41 | Text( 42 | text = stringResource(R.string.trash_deletion_warning), 43 | modifier = Modifier.weight(1f), 44 | style = MaterialTheme.typography.titleSmall, 45 | color = MaterialTheme.colorScheme.onSurfaceVariant 46 | ) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/trashed/components/EmptyTrash.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.presentation.trashed.components 7 | 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.material.icons.Icons 13 | import androidx.compose.material.icons.outlined.Delete 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.dp 22 | import com.dot.gallery.R 23 | 24 | @Composable 25 | fun EmptyTrash( 26 | modifier: Modifier = Modifier, 27 | title: String = stringResource(R.string.empty_trash_title), 28 | ) { 29 | Column( 30 | modifier = modifier.padding(top = 64.dp), 31 | horizontalAlignment = Alignment.CenterHorizontally, 32 | verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically) 33 | ) { 34 | Icon( 35 | modifier = Modifier 36 | .size(128.dp), 37 | imageVector = Icons.Outlined.Delete, 38 | contentDescription = stringResource(R.string.empty_trash_cd), 39 | tint = MaterialTheme.colorScheme.primary 40 | ) 41 | Text( 42 | text = title, 43 | style = MaterialTheme.typography.titleLarge 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/util/ExifMetadata.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.presentation.util 7 | 8 | import androidx.exifinterface.media.ExifInterface 9 | import java.math.RoundingMode 10 | import java.text.DecimalFormat 11 | import java.util.Locale 12 | 13 | class ExifMetadata(exifInterface: ExifInterface) { 14 | val manufacturerName: String? = 15 | exifInterface.getAttribute(ExifInterface.TAG_MAKE) 16 | val modelName: String? = 17 | exifInterface.getAttribute(ExifInterface.TAG_MODEL) 18 | val apertureValue: Double = 19 | exifInterface.getAttributeDouble(ExifInterface.TAG_APERTURE_VALUE, 0.0) 20 | 21 | val focalLength: Double = 22 | exifInterface.getAttributeDouble(ExifInterface.TAG_FOCAL_LENGTH, 0.0) 23 | val isoValue: Int = 24 | exifInterface.getAttributeInt(ExifInterface.TAG_ISO_SPEED, 0) 25 | val imageWidth: Int = 26 | exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, -1) 27 | val imageHeight: Int = 28 | exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, -1) 29 | val imageMp: String 30 | get() { 31 | val roundingMP = DecimalFormat("#.#").apply { roundingMode = RoundingMode.DOWN } 32 | return roundingMP.format(imageWidth * imageHeight / 1024000.0) 33 | } 34 | 35 | val imageDescription: String? = 36 | exifInterface.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION) 37 | 38 | val lensDescription: String? 39 | get() { 40 | return if (!manufacturerName.isNullOrEmpty() && !modelName.isNullOrEmpty() && apertureValue != 0.0) { 41 | "$manufacturerName $modelName - f/$apertureValue - $imageMp MP" 42 | } else null 43 | } 44 | 45 | /** 46 | * 0 - latitude 47 | * 1 - longitude 48 | */ 49 | val gpsLatLong: DoubleArray? = 50 | exifInterface.latLong 51 | 52 | val formattedCords: String? 53 | get() = if (gpsLatLong != null) String.format( 54 | Locale.getDefault(), "%.3f, %.3f", gpsLatLong[0], gpsLatLong[1] 55 | ) else null 56 | 57 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/util/Geolocation.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.util 2 | 3 | import android.location.Address 4 | import android.location.Geocoder 5 | import android.os.Build 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.platform.LocalContext 8 | import androidx.core.text.isDigitsOnly 9 | 10 | @Composable 11 | fun rememberGeocoder(): Geocoder? { 12 | val geocoder = Geocoder(LocalContext.current) 13 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && Geocoder.isPresent()) 14 | geocoder else null 15 | } 16 | 17 | fun Geocoder.getLocation(lat: Double, long: Double, onLocationFound: (Address?) -> Unit) { 18 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 19 | getFromLocation( 20 | /* latitude = */ lat, /* longitude = */ long, /* maxResults = */ 1 21 | ) { 22 | if (it.isEmpty()) onLocationFound(null) 23 | else onLocationFound(it.first()) 24 | } 25 | } else { 26 | onLocationFound(null) 27 | } 28 | } 29 | 30 | val Address.formattedAddress: String get() { 31 | var address = "" 32 | if (!featureName.isNullOrBlank() && !featureName.isDigitsOnly()) address += featureName 33 | else if (!subLocality.isNullOrBlank()) address += subLocality 34 | if (!locality.isNullOrBlank()) { 35 | address += if (address.isEmpty()) locality 36 | else ", $locality" 37 | } 38 | if (!countryName.isNullOrBlank()) { 39 | address += if (address.isEmpty()) countryName 40 | else ", $countryName" 41 | } 42 | 43 | return address 44 | } 45 | 46 | val Address.locationTag: String get() = 47 | if (!featureName.isNullOrBlank() && !featureName.isDigitsOnly()) featureName 48 | else if (!subLocality.isNullOrBlank()) subLocality 49 | else locality -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/util/LogExt.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("KotlinConstantConditions") 2 | 3 | package com.dot.gallery.feature_node.presentation.util 4 | 5 | import android.util.Log 6 | import com.dot.gallery.BuildConfig 7 | 8 | fun printInfo(message: Any) { 9 | Log.i("GalleryInfo", message.toString()) 10 | } 11 | 12 | fun printDebug(message: Any) { 13 | printDebug(message.toString()) 14 | } 15 | 16 | fun printDebug(message: String) { 17 | if (BuildConfig.BUILD_TYPE != "release") { 18 | Log.d("GalleryInfo", message) 19 | } 20 | } 21 | 22 | fun printError(message: String) { 23 | Log.e("GalleryInfo", message) 24 | } 25 | 26 | fun printWarning(message: String) { 27 | Log.w("GalleryInfo", message) 28 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/util/MapBoxURL.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.presentation.util 7 | 8 | import android.util.Size 9 | import com.dot.gallery.BuildConfig 10 | 11 | object MapBoxURL { 12 | 13 | private const val apiKey: String = BuildConfig.MAPS_TOKEN 14 | 15 | operator fun invoke( 16 | latitude: Double, 17 | longitude: Double, 18 | darkTheme: Boolean, 19 | zoom: Double = 11.21, 20 | size: Size = Size(300,200) 21 | ): String { 22 | val themeStyle = if (darkTheme) "dark-v10" else "outdoors-v11" 23 | return "https://api.mapbox.com/styles/v1/mapbox/" + 24 | "$themeStyle/static/pin-s+555555(" + 25 | "$longitude,$latitude)/$longitude,$latitude," + 26 | "$zoom,0/${size.width}x${size.height}@2x" + 27 | "?access_token=${apiKey}" 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/util/NavigationItem.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.presentation.util 7 | 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | 10 | data class NavigationItem( 11 | val name: String, 12 | val route: String, 13 | val icon: ImageVector, 14 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/util/Screen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.presentation.util 7 | 8 | sealed class Screen(val route: String) { 9 | data object TimelineScreen : Screen("timeline_screen") 10 | data object AlbumsScreen : Screen("albums_screen") 11 | 12 | data object AlbumViewScreen : Screen("album_view_screen") { 13 | 14 | fun albumAndName() = "$route?albumId={albumId}&albumName={albumName}" 15 | 16 | } 17 | data object MediaViewScreen : Screen("media_screen") { 18 | 19 | fun idAndTarget() = "$route?mediaId={mediaId}&target={target}" 20 | 21 | fun idAndAlbum() = "$route?mediaId={mediaId}&albumId={albumId}" 22 | 23 | fun idAndQuery() = "$route?mediaId={mediaId}&query={query}" 24 | 25 | fun idAndCategory() = "$route?mediaId={mediaId}&category={category}" 26 | 27 | fun idAndCategory(id: Long, category: String) = "$route?mediaId=$id&category=$category" 28 | } 29 | 30 | data object TrashedScreen : Screen("trashed_screen") 31 | data object FavoriteScreen : Screen("favorite_screen") 32 | 33 | data object SettingsScreen : Screen("settings_screen") 34 | data object IgnoredScreen : Screen("ignored_screen") 35 | data object IgnoredSetupScreen : Screen("ignored_setup_screen") 36 | 37 | data object SetupScreen: Screen("setup_screen") 38 | 39 | data object VaultScreen : Screen("vault_screen") 40 | 41 | data object LibraryScreen : Screen("library_screen") 42 | 43 | data object CategoriesScreen : Screen("categories_screen") 44 | 45 | data object CategoryViewScreen : Screen("category_view_screen") { 46 | 47 | fun category() = "$route?category={category}" 48 | 49 | fun category(string: String) = "$route?category=$string" 50 | 51 | } 52 | 53 | data object DateFormatScreen : Screen("date_format_screen") 54 | 55 | operator fun invoke() = route 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/util/SharedElementsExt.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("CONTEXT_RECEIVERS_DEPRECATED") 2 | 3 | package com.dot.gallery.feature_node.presentation.util 4 | 5 | 6 | import androidx.compose.animation.AnimatedVisibilityScope 7 | import androidx.compose.animation.ExperimentalSharedTransitionApi 8 | import androidx.compose.animation.SharedTransitionScope 9 | import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion.contentSize 10 | import androidx.compose.animation.core.tween 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.ui.Modifier 14 | import com.dot.gallery.core.Settings 15 | import com.dot.gallery.feature_node.domain.model.Album 16 | import com.dot.gallery.feature_node.domain.model.Media 17 | 18 | context(SharedTransitionScope) 19 | @Composable 20 | @OptIn(ExperimentalSharedTransitionApi::class) 21 | fun Modifier.mediaSharedElement( 22 | media: T, 23 | animatedVisibilityScope: AnimatedVisibilityScope 24 | ): Modifier = mediaSharedElement(key = media.idLessKey, animatedVisibilityScope = animatedVisibilityScope) 25 | 26 | context(SharedTransitionScope) 27 | @Composable 28 | @OptIn(ExperimentalSharedTransitionApi::class) 29 | fun Modifier.mediaSharedElement( 30 | album: Album, 31 | animatedVisibilityScope: AnimatedVisibilityScope 32 | ): Modifier = mediaSharedElement(key = album.idLessKey, animatedVisibilityScope = animatedVisibilityScope) 33 | 34 | context(SharedTransitionScope) 35 | @Composable 36 | @OptIn(ExperimentalSharedTransitionApi::class) 37 | private fun Modifier.mediaSharedElement( 38 | key: String, 39 | animatedVisibilityScope: AnimatedVisibilityScope 40 | ): Modifier { 41 | val shouldAnimate by Settings.Misc.rememberSharedElements() 42 | val boundsModifier = sharedBounds( 43 | rememberSharedContentState(key = "media_$key"), 44 | animatedVisibilityScope = animatedVisibilityScope, 45 | placeHolderSize = contentSize, 46 | boundsTransform = { _, _ -> tween(250) } 47 | ) 48 | return if (shouldAnimate) boundsModifier else Modifier 49 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/util/ViewScreenConstants.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.util 2 | 3 | import androidx.compose.ui.unit.Dp 4 | import androidx.compose.ui.unit.dp 5 | import com.composables.core.SheetDetent 6 | 7 | object ViewScreenConstants { 8 | val BOTTOM_BAR_HEIGHT = 100.dp 9 | 10 | fun ImageOnly(height: () -> Dp = { BOTTOM_BAR_HEIGHT }) = 11 | SheetDetent("imageOnly") { _, _ -> 12 | height() 13 | } 14 | 15 | @Suppress("FunctionName") 16 | fun FullyExpanded(setHeight: (Dp) -> Unit) = 17 | SheetDetent("fully-expanded") { _, sheetHeight -> 18 | setHeight(sheetHeight) 19 | sheetHeight 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/util/WindowInsetsControllerExt.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.presentation.util 7 | 8 | import androidx.core.view.WindowInsetsCompat 9 | import androidx.core.view.WindowInsetsControllerCompat 10 | 11 | fun WindowInsetsControllerCompat.toggleSystemBars(show: Boolean) { 12 | if (show) show(WindowInsetsCompat.Type.systemBars()) 13 | else hide(WindowInsetsCompat.Type.systemBars()) 14 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/util/launchMap.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.feature_node.presentation.util 7 | 8 | import android.content.Context 9 | import android.content.Intent 10 | import android.net.Uri 11 | import com.dot.gallery.R 12 | 13 | fun Context.launchMap(lat: Double, lang: Double) { 14 | startActivity( 15 | Intent(Intent.ACTION_VIEW).apply { 16 | data = Uri.parse("geo:0,0?q=$lat,$lang(${getString(R.string.media_location)})") 17 | } 18 | ) 19 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/vault/VaultScreens.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.feature_node.presentation.vault 2 | 3 | sealed class VaultScreens(val route: String) { 4 | data object VaultSetup : VaultScreens("vault_setup") 5 | data object VaultDisplay : VaultScreens("vault_display") 6 | 7 | data object EncryptedMediaViewScreen : VaultScreens("vault_media_view_screen") { 8 | fun id() = "$route?mediaId={mediaId}" 9 | 10 | fun id(id: Long) = "$route?mediaId=$id" 11 | } 12 | 13 | data object LoadingScreen : VaultScreens("vault_loading_screen") 14 | 15 | operator fun invoke() = route 16 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/feature_node/presentation/wallpaper/SetWallpaperActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | package com.dot.gallery.feature_node.presentation.wallpaper 6 | 7 | import android.content.ActivityNotFoundException 8 | import android.content.Intent 9 | import android.os.Bundle 10 | import android.widget.Toast 11 | import androidx.activity.ComponentActivity 12 | import com.dot.gallery.R 13 | 14 | /** 15 | * This is a dummy activity that redirects the Intent action ATTACH_DATA 16 | * to the system's CROP_AND_SET_WALLPAPER 17 | */ 18 | class SetWallpaperActivity : ComponentActivity() { 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | 23 | // Image uri received via intent 24 | val imageUri = intent.data 25 | 26 | if (imageUri != null) { 27 | // Launch the system CROP_AND_SET_WALLPAPER app 28 | val launchIntent = Intent("android.service.wallpaper.CROP_AND_SET_WALLPAPER").apply { 29 | addCategory(Intent.CATEGORY_DEFAULT) 30 | setDataAndType(imageUri, "image/*") 31 | putExtra("mimeType", "image/*") 32 | // Make sure uri permission is granted 33 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 34 | } 35 | try { 36 | startActivity(launchIntent) 37 | } catch (_: ActivityNotFoundException) { 38 | Toast.makeText(this, getString(R.string.set_wallpaper_error), Toast.LENGTH_SHORT).show() 39 | } 40 | } 41 | // Just finish the activity after launching the intent 42 | // as there's no need of its existence 43 | finish() 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/ui/core/Icons.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.ui.core 2 | 3 | object Icons -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/ui/core/icons/InkMarker.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.ui.core.icons 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | import com.dot.gallery.ui.core.Icons 9 | 10 | val Icons.InkMarker: ImageVector 11 | get() { 12 | if (_InkMarker != null) { 13 | return _InkMarker!! 14 | } 15 | _InkMarker = ImageVector.Builder( 16 | name = "InkMarker", 17 | defaultWidth = 24.dp, 18 | defaultHeight = 24.dp, 19 | viewportWidth = 960f, 20 | viewportHeight = 960f 21 | ).apply { 22 | path(fill = SolidColor(Color(0xFF000000))) { 23 | moveTo(272f, 856f) 24 | lineTo(234f, 818f) 25 | lineTo(192f, 860f) 26 | quadTo(173f, 879f, 145.5f, 879.5f) 27 | quadTo(118f, 880f, 100f, 860f) 28 | quadTo(81f, 841f, 81f, 814f) 29 | quadTo(81f, 787f, 100f, 768f) 30 | lineTo(142f, 726f) 31 | lineTo(104f, 686f) 32 | lineTo(658f, 132f) 33 | quadTo(670f, 120f, 687f, 120f) 34 | quadTo(704f, 120f, 716f, 132f) 35 | lineTo(828f, 244f) 36 | quadTo(840f, 256f, 840f, 273f) 37 | quadTo(840f, 290f, 828f, 302f) 38 | lineTo(272f, 856f) 39 | close() 40 | moveTo(444f, 460f) 41 | lineTo(216f, 686f) 42 | lineTo(274f, 744f) 43 | lineTo(500f, 516f) 44 | lineTo(444f, 460f) 45 | close() 46 | } 47 | }.build() 48 | 49 | return _InkMarker!! 50 | } 51 | 52 | @Suppress("ObjectPropertyName") 53 | private var _InkMarker: ImageVector? = null 54 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/ui/core/icons/Ink_eraser.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.ui.core.icons 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap 7 | import androidx.compose.ui.graphics.StrokeJoin 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.unit.dp 11 | import com.dot.gallery.ui.core.Icons 12 | 13 | public val Icons.Ink_Eraser: ImageVector 14 | get() { 15 | if (_Ink_Eraser != null) { 16 | return _Ink_Eraser!! 17 | } 18 | _Ink_Eraser = ImageVector.Builder( 19 | name = "Ink_eraser", 20 | defaultWidth = 24.dp, 21 | defaultHeight = 24.dp, 22 | viewportWidth = 960f, 23 | viewportHeight = 960f 24 | ).apply { 25 | path( 26 | fill = SolidColor(Color.Black), 27 | fillAlpha = 1.0f, 28 | stroke = null, 29 | strokeAlpha = 1.0f, 30 | strokeLineWidth = 1.0f, 31 | strokeLineCap = StrokeCap.Butt, 32 | strokeLineJoin = StrokeJoin.Miter, 33 | strokeLineMiter = 1.0f, 34 | pathFillType = PathFillType.NonZero 35 | ) { 36 | moveTo(690f, 720f) 37 | horizontalLineToRelative(190f) 38 | verticalLineToRelative(80f) 39 | horizontalLineTo(610f) 40 | close() 41 | moveToRelative(-500f, 80f) 42 | lineToRelative(-85f, -85f) 43 | quadToRelative(-23f, -23f, -23.5f, -57f) 44 | reflectiveQuadToRelative(22.5f, -58f) 45 | lineToRelative(440f, -456f) 46 | quadToRelative(23f, -24f, 56.5f, -24f) 47 | reflectiveQuadToRelative(56.5f, 23f) 48 | lineToRelative(199f, 199f) 49 | quadToRelative(23f, 23f, 23f, 57f) 50 | reflectiveQuadToRelative(-23f, 57f) 51 | lineTo(520f, 800f) 52 | close() 53 | moveToRelative(296f, -80f) 54 | lineToRelative(314f, -322f) 55 | lineToRelative(-198f, -198f) 56 | lineToRelative(-442f, 456f) 57 | lineToRelative(64f, 64f) 58 | close() 59 | moveToRelative(-6f, -240f) 60 | } 61 | }.build() 62 | return _Ink_Eraser!! 63 | } 64 | 65 | private var _Ink_Eraser: ImageVector? = null 66 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/ui/core/icons/Star.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.ui.core.icons 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType.Companion.NonZero 5 | import androidx.compose.ui.graphics.SolidColor 6 | import androidx.compose.ui.graphics.StrokeCap.Companion.Butt 7 | import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter 8 | import androidx.compose.ui.graphics.vector.ImageVector 9 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 10 | import androidx.compose.ui.graphics.vector.path 11 | import androidx.compose.ui.unit.dp 12 | import com.dot.gallery.ui.core.Icons 13 | 14 | val Icons.Star: ImageVector 15 | get() { 16 | if (star != null) { 17 | return star!! 18 | } 19 | star = Builder(name = "Star", defaultWidth = 167.0.dp, defaultHeight = 139.0.dp, 20 | viewportWidth = 167.0f, viewportHeight = 139.0f).apply { 21 | path(fill = SolidColor(Color(0xff000000)), stroke = null, strokeLineWidth = 0.0f, 22 | strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, 23 | pathFillType = NonZero) { 24 | moveTo(3.691f, 50.274f) 25 | curveTo(-11.842f, 20.417f, 24.458f, -9.886f, 60.224f, 3.081f) 26 | lineTo(66.137f, 5.225f) 27 | curveTo(77.057f, 9.184f, 89.454f, 9.184f, 100.373f, 5.225f) 28 | lineTo(106.287f, 3.081f) 29 | curveTo(142.052f, -9.886f, 178.353f, 20.417f, 162.819f, 50.274f) 30 | lineTo(160.251f, 55.21f) 31 | curveTo(155.509f, 64.325f, 155.509f, 74.675f, 160.251f, 83.79f) 32 | lineTo(162.819f, 88.726f) 33 | curveTo(178.353f, 118.583f, 142.052f, 148.886f, 106.287f, 135.919f) 34 | lineTo(100.373f, 133.775f) 35 | curveTo(89.454f, 129.816f, 77.057f, 129.816f, 66.137f, 133.775f) 36 | lineTo(60.224f, 135.919f) 37 | curveTo(24.458f, 148.886f, -11.842f, 118.583f, 3.691f, 88.726f) 38 | lineTo(6.259f, 83.79f) 39 | curveTo(11.002f, 74.675f, 11.002f, 64.325f, 6.259f, 55.21f) 40 | lineTo(3.691f, 50.274f) 41 | close() 42 | } 43 | } 44 | .build() 45 | return star!! 46 | } 47 | 48 | private var star: ImageVector? = null 49 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/ui/core/icons/Stylus.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.ui.core.icons 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | import com.dot.gallery.ui.core.Icons 9 | 10 | val Icons.Stylus: ImageVector 11 | get() { 12 | if (_Stylus != null) { 13 | return _Stylus!! 14 | } 15 | _Stylus = ImageVector.Builder( 16 | name = "Stylus", 17 | defaultWidth = 24.dp, 18 | defaultHeight = 24.dp, 19 | viewportWidth = 960f, 20 | viewportHeight = 960f 21 | ).apply { 22 | path(fill = SolidColor(Color(0xFF000000))) { 23 | moveTo(167f, 840f) 24 | quadTo(146f, 845f, 130.5f, 829.5f) 25 | quadTo(115f, 814f, 120f, 793f) 26 | lineTo(160f, 602f) 27 | lineTo(358f, 800f) 28 | lineTo(167f, 840f) 29 | close() 30 | moveTo(358f, 800f) 31 | lineTo(160f, 602f) 32 | lineTo(618f, 144f) 33 | quadTo(641f, 121f, 675f, 121f) 34 | quadTo(709f, 121f, 732f, 144f) 35 | lineTo(816f, 228f) 36 | quadTo(839f, 251f, 839f, 285f) 37 | quadTo(839f, 319f, 816f, 342f) 38 | lineTo(358f, 800f) 39 | close() 40 | moveTo(675f, 200f) 41 | lineTo(261f, 614f) 42 | lineTo(346f, 699f) 43 | lineTo(760f, 285f) 44 | quadTo(760f, 285f, 760f, 285f) 45 | quadTo(760f, 285f, 760f, 285f) 46 | lineTo(675f, 200f) 47 | quadTo(675f, 200f, 675f, 200f) 48 | quadTo(675f, 200f, 675f, 200f) 49 | close() 50 | } 51 | }.build() 52 | 53 | return _Stylus!! 54 | } 55 | 56 | @Suppress("ObjectPropertyName") 57 | private var _Stylus: ImageVector? = null 58 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/ui/theme/Dimens.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.ui.theme 7 | 8 | import androidx.compose.runtime.Immutable 9 | import androidx.compose.ui.unit.Dp 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Immutable 13 | sealed class Dimens(val size: Dp) { 14 | data object Photo : Dimens(size = 100.dp) 15 | data object Album : Dimens(size = 178.dp) 16 | 17 | operator fun invoke(): Dp = size 18 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 IacobIacob01 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.dot.gallery.ui.theme 7 | 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material3.Shapes 10 | import androidx.compose.ui.unit.dp 11 | 12 | val Shapes = Shapes( 13 | extraSmall = RoundedCornerShape(0.dp), 14 | small = RoundedCornerShape(2.dp), 15 | medium = RoundedCornerShape(8.dp), 16 | large = RoundedCornerShape(16.dp) 17 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/dot/gallery/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_donate.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_gallery_thumbnail.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_github.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground_monochrome.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_media_manage.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_remove.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_scroll_arrow.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/image_sample_1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/app/src/main/res/drawable/image_sample_1.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/image_sample_2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/app/src/main/res/drawable/image_sample_2.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/image_sample_3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/app/src/main/res/drawable/image_sample_3.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/image_sample_4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/app/src/main/res/drawable/image_sample_4.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_border_tv.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/album_pin_frame.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | 26 | 27 | 36 | 37 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_photo_editor_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 20 | 21 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_photo_editor_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 23 | 24 | 25 | 26 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/release/generated/baselineProfiles/startup-prof.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/app/src/release/generated/baselineProfiles/startup-prof.txt -------------------------------------------------------------------------------- /app/src/test/java/com/dot/gallery/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.dot.gallery 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /baselineProfile/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /baselineProfile/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.api.dsl.ManagedVirtualDevice 2 | 3 | plugins { 4 | id("com.android.test") 5 | id("org.jetbrains.kotlin.android") 6 | id("androidx.baselineprofile") 7 | } 8 | 9 | android { 10 | buildTypes { 11 | create("staging") { 12 | } 13 | create("benchmarkStaging") { 14 | } 15 | create("nonMinifiedStaging") { 16 | } 17 | } 18 | flavorDimensions += listOf("abi") 19 | productFlavors { 20 | create("arm64-v8a") { 21 | dimension = "abi" 22 | ndk.abiFilters.add("arm64-v8a") 23 | } 24 | create("armeabi-v7a") { 25 | dimension = "abi" 26 | ndk.abiFilters.add("armeabi-v7a") 27 | } 28 | create("x86_64") { 29 | dimension = "abi" 30 | ndk.abiFilters.add("x86_64") 31 | } 32 | create("x86") { 33 | dimension = "abi" 34 | ndk.abiFilters.add("x86") 35 | } 36 | } 37 | namespace = "com.dot.baselineprofile" 38 | compileSdk = 34 39 | 40 | compileOptions { 41 | sourceCompatibility = JavaVersion.VERSION_1_8 42 | targetCompatibility = JavaVersion.VERSION_1_8 43 | } 44 | 45 | kotlinOptions { 46 | jvmTarget = "1.8" 47 | } 48 | 49 | defaultConfig { 50 | minSdk = 30 51 | targetSdk = 34 52 | 53 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 54 | } 55 | 56 | targetProjectPath = ":app" 57 | 58 | testOptions.managedDevices.devices { 59 | create("pixel6Api31") { 60 | device = "Pixel 6" 61 | apiLevel = 31 62 | systemImageSource = "aosp" 63 | } 64 | } 65 | } 66 | 67 | // This is the configuration block for the Baseline Profile plugin. 68 | // You can specify to run the generators on a managed devices or connected devices. 69 | baselineProfile { 70 | //managedDevices += "pixel6Api31" 71 | useConnectedDevices = true 72 | } 73 | 74 | dependencies { 75 | implementation(libs.androidx.test.ext.junit) 76 | implementation(libs.espresso.core) 77 | implementation(libs.androidx.uiautomator) 78 | implementation(libs.androidx.benchmark.macro.junit4) 79 | } -------------------------------------------------------------------------------- /baselineProfile/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.androidApplication) apply false 4 | alias(libs.plugins.androidLibrary) apply false 5 | alias(libs.plugins.kotlinAndroid) apply false 6 | alias(libs.plugins.hiltAndroid) apply false 7 | alias(libs.plugins.kspAndroid) apply false 8 | alias(libs.plugins.roomPlugin) apply false 9 | alias(libs.plugins.androidTest) apply false 10 | alias(libs.plugins.baselineProfilePlugin) apply false 11 | alias(libs.plugins.kotlin.compose.compiler) apply false 12 | alias(libs.plugins.kotlinSerialization) apply false 13 | } -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /app/src/main/res/values/string*.xml 3 | translation: /app/src/main/res/values-%android_code%/%original_file_name% 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/10216.txt: -------------------------------------------------------------------------------- 1 | What's Changed 2 | 3 | - Added Settings Screen 4 | - Added Translations for: English, Chinese-Simplified, Galician, Romanian, Spanish and Turkish 5 | - Bug Fixes 6 | - Improved App Bar 7 | - Added support for launching Gallery from Camera app 8 | - Added Album pinning feature 9 | - Animate Photos Screen 10 | - Improve media grouping in Photos Screen 11 | 12 | 13 | Full Changelog: "https://github.com/IacobIonut01/Gallery/releases/tag/v1.0.2 14 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/20111.txt: -------------------------------------------------------------------------------- 1 | New Features 2 | Added option to create new albums 3 | Fixed rotate button 4 | Added option to disable vibration feedback 5 | Fixed video screen timeout 6 | Added blacklist feature 7 | Fixed today listing on timeline 8 | Added move feature 9 | Fixed moving files not updating the media 10 | Added copy feature 11 | 12 | Full Changelog: https://github.com/IacobIonut01/Gallery/releases/tag/2.0.1-20111 -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/21000.txt: -------------------------------------------------------------------------------- 1 | New Features 2 | Gallery: Added the ability to use Albums as the start screen. 3 | Settings: Added a new UI for Donations. 4 | Gallery: Introduced an initial version of the Editor. 5 | Editor: Improved the crop layout and added a Filters and More section. 6 | Gallery: Added support for JPEG-XL images. 7 | Gallery: Added the ability to set the launch screen in Settings. 8 | 9 | Improvements 10 | AlbumSizeScreen: Slightly improved the UI. 11 | Gallery: Preloaded timeline and albums viewmodels outside navigation for better performance. 12 | MediaView: Dismissed trash sheet on action complete for better user experience. 13 | Misc: Updated 'favorite' string for better clarity. 14 | Gallery: Album thumbnails are now loaded from URI for better performance. 15 | Media: Improved the Copy Sheet. 16 | Gallery: Fixed Sticky Header. 17 | SelectionSheet: Fixed layout on big screens for better user experience. 18 | Gallery: Improved overall speed and reduced media parsing calls for better performance. 19 | MediaView: Made file renaming more obvious for better user experience. 20 | 21 | Bug Fixes 22 | Gallery: Fixed MediaStore Observer. 23 | Gallery: Fixed miscellaneous issues. 24 | Gallery: Moved Media Manager request to Setup to fix potential issues. 25 | Gallery: Fixed various bugs and migrated to Coil for better performance and stability. 26 | 27 | Full Changelog: https://github.com/IacobIonut01/Gallery/releases/tag/2.1.0-21000 -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/21009.txt: -------------------------------------------------------------------------------- 1 | New Features 2 | Gallery: Added video player audio toggle button 3 | Gallery: Automatically disable blur on battery saver 4 | 5 | Improvements 6 | Gallery: Reduced lag for some devices 7 | 8 | Bug Fixes 9 | Gallery: Fixed classic navigation bar colors 10 | Gallery: Fixed some edge cases when navigation bar is showing incorrectly 11 | Gallery: Fix missing 'Open as' for media 12 | 13 | Full Changelog: https://github.com/IacobIonut01/Gallery/releases/tag/2.1.0-21009 -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/30033.txt: -------------------------------------------------------------------------------- 1 | Added 2 | Vault - A secure way to hide and encrypt your photos 3 | Ignored Albums - A safe and easy way to ignore certain albums or automatically ignore a bunch of them with Regex 4 | Option to Move the album from trash from Album screen / Long press the album 5 | Option to not hide the searchbar and/or the navigationbar while scrolling 6 | New gesture: Drag down to close media viewer 7 | New UI for Setup Wizard 8 | Improved 9 | UI Performance while viewing media grid 10 | Media Content parsing and updating 11 | MediaView screen - New UI and animations 12 | Scroll Thumb 13 | Slightly updated UI Design on Library tab 14 | Empty and loading animations for media and albums 15 | Detect initial grid size for bigger screens 16 | Album sorting toggle 17 | Re-arranged Settings screen 18 | Fixed 19 | Lags and hangs while fast scrolling the media grid 20 | Major lags while scrolling and updating the Media Store (e.g., when new images are being added to your gallery, and you want to scroll the grid) 21 | Small fixes to the video player 22 | Changed 23 | Moved to a new image loading library (sketch) 24 | Moved to a new image subsampling library (com.github.panpf.zoomimage) 25 | Removed 26 | Temporarily removed the Image Editor - a new one will be coming soon -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 |

Gallery for Android is a light-weight Media Gallery app for Android made with Jetpack Compose. It supports albums, favorites and more. It also features an integrated video player.

2 | 3 |

 

4 | 5 |

Features

6 | 7 |
    8 |
  • Media Timeline screen where you can see all the photos videos 9 |
      10 |
    • The Timeline screen is visible from the Main screen and inside each album
    • 11 |
    • Media is separated in groups by date
    • 12 |
    13 |
  • 14 |
  • Multi-Select photos and videos to perform batch operations like 15 |
      16 |
    • Mark as favorite
    • 17 |
    • Move to trash
    • 18 |
    • Share
    • 19 |
    20 |
  • 21 |
  • Sort and Pin Albums 22 |
      23 |
    • Sort  by Date, Label
    • 24 |
    • Pin on top most loved albums
    • 25 |
    26 |
  • 27 |
  • Quickly Access all the Favorites 
  • 28 |
  • Manage the Trashed Items from a single screen
  • 29 |
  • Custom Video Player powered by Media3 ExoPlayer
  • 30 |
  • Modern Material 3 Design Guidelines powered by Jetpack Compose
  • 31 |
  • Open Source
  • 32 |
  • Active Development
  • 33 |
34 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/11.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/12.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/13.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Light-weight Media Gallery -------------------------------------------------------------------------------- /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=-Xmx4G -Dfile.encoding=UTF-8 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 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | org.gradle.unsafe.configuration-cache=true 25 | android.suppressUnsupportedCompileSdk=35 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /libs/cropper/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /libs/cropper/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidLibrary) 3 | alias(libs.plugins.kotlinAndroid) 4 | alias(libs.plugins.kotlin.compose.compiler) 5 | } 6 | 7 | android { 8 | namespace = "com.smarttoolfactory.cropper" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | minSdk = 30 13 | } 14 | 15 | buildTypes { 16 | create("staging") { 17 | initWith(getByName("release")) 18 | isMinifyEnabled = false 19 | isShrinkResources = false 20 | } 21 | } 22 | 23 | compileOptions { 24 | sourceCompatibility = JavaVersion.VERSION_17 25 | targetCompatibility = JavaVersion.VERSION_17 26 | } 27 | 28 | kotlinOptions { 29 | jvmTarget = "17" 30 | } 31 | 32 | composeCompiler { 33 | includeSourceInformation = true 34 | } 35 | 36 | buildFeatures { 37 | compose = true 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation(libs.compose.foundation) 43 | implementation(libs.compose.ui) 44 | implementation(libs.compose.runtime) 45 | implementation(libs.compose.material3) 46 | implementation(libs.compose.material.icons.extended) 47 | 48 | implementation(project(":libs:gesture")) 49 | } -------------------------------------------------------------------------------- /libs/cropper/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/TouchRegion.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper 2 | 3 | /** 4 | * Enum for detecting which section of Composable user has initially touched 5 | */ 6 | enum class TouchRegion { 7 | TopLeft, TopRight, BottomLeft, BottomRight, LeftEdge, RightEdge, TopEdge, BottomEdge, Inside, None 8 | } 9 | 10 | fun handlesTouched(touchRegion: TouchRegion) = touchRegion == TouchRegion.TopLeft || 11 | touchRegion == TouchRegion.TopRight || 12 | touchRegion == TouchRegion.BottomLeft || 13 | touchRegion == TouchRegion.BottomRight || 14 | touchRegion == TouchRegion.LeftEdge || 15 | touchRegion == TouchRegion.RightEdge || 16 | touchRegion == TouchRegion.TopEdge || 17 | touchRegion == TouchRegion.BottomEdge 18 | -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/draw/ImageDrawCanvas.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.draw 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.graphics.ImageBitmap 7 | import androidx.compose.ui.unit.IntOffset 8 | import androidx.compose.ui.unit.IntSize 9 | import kotlin.math.roundToInt 10 | 11 | /** 12 | * Draw image to be cropped 13 | */ 14 | @Composable 15 | internal fun ImageDrawCanvas( 16 | modifier: Modifier, 17 | imageBitmap: ImageBitmap, 18 | imageWidth: Int, 19 | imageHeight: Int 20 | ) { 21 | Canvas(modifier = modifier) { 22 | 23 | val canvasWidth = size.width.roundToInt() 24 | val canvasHeight = size.height.roundToInt() 25 | 26 | drawImage( 27 | image = imageBitmap, 28 | srcSize = IntSize(imageBitmap.width, imageBitmap.height), 29 | dstSize = IntSize(imageWidth, imageHeight), 30 | dstOffset = IntOffset( 31 | x = (canvasWidth - imageWidth) / 2, 32 | y = (canvasHeight - imageHeight) / 2 33 | ) 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/model/AspectRatios.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.model 2 | 3 | import com.smarttoolfactory.cropper.util.createRectShape 4 | 5 | /** 6 | * Aspect ratio list with pre-defined aspect ratios 7 | */ 8 | val aspectRatios = listOf( 9 | CropAspectRatio( 10 | title = "9:16", 11 | shape = createRectShape(AspectRatio(9 / 16f)), 12 | aspectRatio = AspectRatio(9 / 16f) 13 | ), 14 | CropAspectRatio( 15 | title = "2:3", 16 | shape = createRectShape(AspectRatio(2 / 3f)), 17 | aspectRatio = AspectRatio(2 / 3f) 18 | ), 19 | CropAspectRatio( 20 | title = "Original", 21 | shape = createRectShape(AspectRatio.Original), 22 | aspectRatio = AspectRatio.Original 23 | ), 24 | CropAspectRatio( 25 | title = "1:1", 26 | shape = createRectShape(AspectRatio(1 / 1f)), 27 | aspectRatio = AspectRatio(1 / 1f) 28 | ), 29 | CropAspectRatio( 30 | title = "16:9", 31 | shape = createRectShape(AspectRatio(16 / 9f)), 32 | aspectRatio = AspectRatio(16 / 9f) 33 | ), 34 | CropAspectRatio( 35 | title = "1.91:1", 36 | shape = createRectShape(AspectRatio(1.91f / 1f)), 37 | aspectRatio = AspectRatio(1.91f / 1f) 38 | ), 39 | CropAspectRatio( 40 | title = "3:2", 41 | shape = createRectShape(AspectRatio(3 / 2f)), 42 | aspectRatio = AspectRatio(3 / 2f) 43 | ), 44 | CropAspectRatio( 45 | title = "3:4", 46 | shape = createRectShape(AspectRatio(3 / 4f)), 47 | aspectRatio = AspectRatio(3 / 4f) 48 | ), 49 | CropAspectRatio( 50 | title = "3:5", 51 | shape = createRectShape(AspectRatio(3 / 5f)), 52 | aspectRatio = AspectRatio(3 / 5f) 53 | ) 54 | ) -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/model/CropAspectRatio.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Shape 5 | 6 | /** 7 | * Model for drawing title with shape for crop selection menu. Aspect ratio is used 8 | * for setting overlay in state and UI 9 | */ 10 | @Immutable 11 | data class CropAspectRatio( 12 | val title: String, 13 | val shape: Shape, 14 | val aspectRatio: AspectRatio = AspectRatio.Original, 15 | val icons: List = listOf() 16 | ) 17 | 18 | /** 19 | * Value class for containing aspect ratio 20 | * and [AspectRatio.Original] for comparing 21 | */ 22 | @Immutable 23 | data class AspectRatio(val value: Float) { 24 | companion object { 25 | val Original = AspectRatio(-1f) 26 | } 27 | } -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/model/CropData.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.geometry.Offset 5 | import androidx.compose.ui.geometry.Rect 6 | 7 | 8 | /** 9 | * Class that contains information about 10 | * current zoom, pan and rotation, and rectangle of zoomed and panned area for cropping [cropRect], 11 | * and area of overlay as[overlayRect] 12 | * 13 | */ 14 | @Immutable 15 | data class CropData( 16 | val zoom: Float = 1f, 17 | val pan: Offset = Offset.Zero, 18 | val rotation: Float = 0f, 19 | val overlayRect: Rect, 20 | val cropRect: Rect 21 | ) 22 | -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/model/CropOutlineContainer.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.model 2 | 3 | /** 4 | * Interface for containing multiple [CropOutline]s, currently selected item and index 5 | * for displaying on settings UI 6 | */ 7 | interface CropOutlineContainer { 8 | var selectedIndex: Int 9 | val outlines: List 10 | val selectedItem: O 11 | get() = outlines[selectedIndex] 12 | val size: Int 13 | get() = outlines.size 14 | } 15 | 16 | /** 17 | * Container for [RectCropShape] 18 | */ 19 | data class RectOutlineContainer( 20 | override var selectedIndex: Int = 0, 21 | override val outlines: List 22 | ) : CropOutlineContainer 23 | 24 | /** 25 | * Container for [RoundedCornerCropShape]s 26 | */ 27 | data class RoundedRectOutlineContainer( 28 | override var selectedIndex: Int = 0, 29 | override val outlines: List 30 | ) : CropOutlineContainer 31 | 32 | /** 33 | * Container for [CutCornerCropShape]s 34 | */ 35 | data class CutCornerRectOutlineContainer( 36 | override var selectedIndex: Int = 0, 37 | override val outlines: List 38 | ) : CropOutlineContainer 39 | 40 | /** 41 | * Container for [OvalCropShape]s 42 | */ 43 | data class OvalOutlineContainer( 44 | override var selectedIndex: Int = 0, 45 | override val outlines: List 46 | ) : CropOutlineContainer 47 | 48 | /** 49 | * Container for [PolygonCropShape]s 50 | */ 51 | data class PolygonOutlineContainer( 52 | override var selectedIndex: Int = 0, 53 | override val outlines: List 54 | ) : CropOutlineContainer 55 | 56 | /** 57 | * Container for [CustomPathOutline]s 58 | */ 59 | data class CustomOutlineContainer( 60 | override var selectedIndex: Int = 0, 61 | override val outlines: List 62 | ) : CropOutlineContainer 63 | 64 | /** 65 | * Container for [ImageMaskOutline]s 66 | */ 67 | data class ImageMaskOutlineContainer( 68 | override var selectedIndex: Int = 0, 69 | override val outlines: List 70 | ) : CropOutlineContainer 71 | -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/model/CropOutlineProperties.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.geometry.Offset 5 | 6 | @Immutable 7 | data class CornerRadiusProperties( 8 | val topStartPercent: Int = 20, 9 | val topEndPercent: Int = 20, 10 | val bottomStartPercent: Int = 20, 11 | val bottomEndPercent: Int = 20 12 | ) 13 | 14 | @Immutable 15 | data class PolygonProperties( 16 | val sides: Int = 6, 17 | val angle: Float = 0f, 18 | val offset: Offset = Offset.Zero 19 | ) 20 | 21 | @Immutable 22 | data class OvalProperties( 23 | val startAngle: Float = 0f, 24 | val sweepAngle: Float = 360f, 25 | val offset: Offset = Offset.Zero 26 | ) 27 | -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/settings/CropType.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.settings 2 | 3 | /** 4 | * Type of cropping operation 5 | * 6 | * If [CropType.Static] is selected overlay is stationary, image is movable. 7 | * If [CropType.Dynamic] is selected overlay can be moved, resized, image is stationary. 8 | */ 9 | enum class CropType { 10 | Static, Dynamic 11 | } -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/settings/Paths.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.settings 2 | 3 | import androidx.compose.ui.graphics.Path 4 | 5 | // TODO Find VectorDrawables as paths(or library to convert to) to add more 6 | object Paths { 7 | val Favorite 8 | get() = Path().apply { 9 | moveTo(12.0f, 21.35f) 10 | relativeLineTo(-1.45f, -1.32f) 11 | cubicTo(5.4f, 15.36f, 2.0f, 12.28f, 2.0f, 8.5f) 12 | cubicTo(2.0f, 5.42f, 4.42f, 3.0f, 7.5f, 3.0f) 13 | relativeCubicTo(1.74f, 0.0f, 3.41f, 0.81f, 4.5f, 2.09f) 14 | cubicTo(13.09f, 3.81f, 14.76f, 3.0f, 16.5f, 3.0f) 15 | cubicTo(19.58f, 3.0f, 22.0f, 5.42f, 22.0f, 8.5f) 16 | relativeCubicTo(0.0f, 3.78f, -3.4f, 6.86f, -8.55f, 11.54f) 17 | lineTo(12.0f, 21.35f) 18 | close() 19 | } 20 | 21 | val Star = Path().apply { 22 | moveTo(12.0f, 17.27f) 23 | lineTo(18.18f, 21.0f) 24 | relativeLineTo(-1.64f, -7.03f) 25 | lineTo(22.0f, 9.24f) 26 | relativeLineTo(-7.19f, -0.61f) 27 | lineTo(12.0f, 2.0f) 28 | lineTo(9.19f, 8.63f) 29 | lineTo(2.0f, 9.24f) 30 | relativeLineTo(5.46f, 4.73f) 31 | lineTo(5.82f, 21.0f) 32 | close() 33 | } 34 | 35 | val Clover = Path().apply { 36 | moveTo(12f, 100f) 37 | cubicTo(12f, 76f, 0f, 77.6142f, 0f, 50f) 38 | cubicTo(0f, 22.3858f, 22.3858f, 0f, 50f, 0f) 39 | cubicTo(77.6142f, 0f, 76f, 12f, 100f, 12f) 40 | cubicTo(124f, 12f, 122.3858f, 0f, 150f, 0f) 41 | cubicTo(177.6142f, 0f, 200f, 22.3858f, 200f, 50f) 42 | cubicTo(200f, 77.6142f, 188f, 76f, 188f, 100f) 43 | cubicTo(188f, 124f, 200f, 122.3858f, 200f, 150f) 44 | cubicTo(200f, 177.6142f, 177.6142f, 200f, 150f, 200f) 45 | cubicTo(122.3858f, 200f, 124f, 188f, 100f, 188f) 46 | cubicTo(76f, 188f, 77.6142f, 200f, 50f, 200f) 47 | cubicTo(22.3858f, 200f, 0f, 177.6142f, 0f, 150f) 48 | cubicTo(0f, 122.3858f, 12f, 124f, 12f, 100f) 49 | close() 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val DefaultBackgroundColor = Color(0x99000000) 6 | val DefaultOverlayColor = Color.Gray 7 | val DefaultHandleColor = Color.White -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/util/DimensionUtil.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.util 2 | 3 | /** 4 | * [Linear Interpolation](https://en.wikipedia.org/wiki/Linear_interpolation) function that moves 5 | * amount from it's current position to start and amount 6 | * @param start of interval 7 | * @param stop of interval 8 | * @param fraction closed unit interval [0, 1] 9 | */ 10 | fun lerp(start: Float, stop: Float, fraction: Float): Float { 11 | return (1 - fraction) * start + fraction * stop 12 | } 13 | 14 | /** 15 | * Scale x1 from start1..end1 range to start2..end2 range 16 | 17 | */ 18 | fun scale(start1: Float, end1: Float, pos: Float, start2: Float, end2: Float) = 19 | lerp(start2, end2, calculateFraction(start1, end1, pos)) 20 | 21 | /** 22 | * Scale x.start, x.endInclusive from a1..b1 range to a2..b2 range 23 | */ 24 | fun scale( 25 | start1: Float, 26 | end1: Float, 27 | range: ClosedFloatingPointRange, 28 | start2: Float, 29 | end2: Float 30 | ) = 31 | scale(start1, end1, range.start, start2, end2)..scale( 32 | start1, 33 | end1, 34 | range.endInclusive, 35 | start2, 36 | end2 37 | ) 38 | 39 | 40 | /** 41 | * Calculate fraction for value between a range [end] and [start] coerced into 0f-1f range 42 | */ 43 | fun calculateFraction(start: Float, end: Float, pos: Float) = 44 | (if (end - start == 0f) 0f else (pos - start) / (end - start)).coerceIn(0f, 1f) 45 | -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/util/OffsetUtil.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.util 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | 5 | 6 | /** 7 | * Coerce an [Offset] x value in [horizontalRange] and y value in [verticalRange] 8 | */ 9 | fun Offset.coerceIn( 10 | horizontalRange: ClosedRange, 11 | verticalRange: ClosedRange 12 | ) = Offset(this.x.coerceIn(horizontalRange), this.y.coerceIn(verticalRange)) -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/util/ZoomLevel.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.util 2 | 3 | /** 4 | * Enum class for zoom levels 5 | */ 6 | enum class ZoomLevel { 7 | Min, Mid, Max 8 | } 9 | -------------------------------------------------------------------------------- /libs/cropper/src/main/java/com/smarttoolfactory/cropper/util/ZoomUtil.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.cropper.util 2 | 3 | import androidx.compose.ui.graphics.GraphicsLayerScope 4 | import com.smarttoolfactory.cropper.state.TransformState 5 | 6 | /** 7 | * Calculate zoom level and zoom value when user double taps 8 | */ 9 | internal fun calculateZoom( 10 | zoomLevel: ZoomLevel, 11 | initial: Float, 12 | min: Float, 13 | max: Float 14 | ): Pair { 15 | 16 | val newZoomLevel: ZoomLevel 17 | val newZoom: Float 18 | 19 | when (zoomLevel) { 20 | ZoomLevel.Mid -> { 21 | newZoomLevel = ZoomLevel.Max 22 | newZoom = max.coerceAtMost(3f) 23 | } 24 | 25 | ZoomLevel.Max -> { 26 | newZoomLevel = ZoomLevel.Min 27 | newZoom = if (min == initial) initial else min 28 | } 29 | 30 | else -> { 31 | newZoomLevel = ZoomLevel.Mid 32 | newZoom = if (min == initial) (min + max.coerceAtMost(3f)) / 2 else initial 33 | } 34 | } 35 | return Pair(newZoomLevel, newZoom) 36 | } 37 | 38 | internal fun getNextZoomLevel(zoomLevel: ZoomLevel): ZoomLevel = when (zoomLevel) { 39 | ZoomLevel.Mid -> { 40 | ZoomLevel.Max 41 | } 42 | 43 | ZoomLevel.Max -> { 44 | ZoomLevel.Min 45 | } 46 | 47 | else -> { 48 | ZoomLevel.Mid 49 | } 50 | } 51 | 52 | /** 53 | * Update graphic layer with [transformState] 54 | */ 55 | internal fun GraphicsLayerScope.update(transformState: TransformState) { 56 | 57 | // Set zoom 58 | val zoom = transformState.zoom 59 | this.scaleX = zoom 60 | this.scaleY = zoom 61 | 62 | // Set pan 63 | val pan = transformState.pan 64 | val translationX = pan.x 65 | val translationY = pan.y 66 | this.translationX = translationX 67 | this.translationY = translationY 68 | 69 | // Set rotation 70 | this.rotationZ = transformState.rotation 71 | } 72 | -------------------------------------------------------------------------------- /libs/cropper/src/main/res/drawable/landscape2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/libs/cropper/src/main/res/drawable/landscape2.jpg -------------------------------------------------------------------------------- /libs/gesture/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /libs/gesture/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidLibrary) 3 | alias(libs.plugins.kotlinAndroid) 4 | alias(libs.plugins.kotlin.compose.compiler) 5 | } 6 | 7 | android { 8 | namespace = "com.smarttoolfactory.gesture" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | minSdk = 30 13 | } 14 | 15 | buildTypes { 16 | create("staging") { 17 | initWith(getByName("release")) 18 | isMinifyEnabled = false 19 | isShrinkResources = false 20 | } 21 | } 22 | 23 | compileOptions { 24 | sourceCompatibility = JavaVersion.VERSION_17 25 | targetCompatibility = JavaVersion.VERSION_17 26 | } 27 | 28 | kotlinOptions { 29 | jvmTarget = "17" 30 | } 31 | 32 | composeCompiler { 33 | includeSourceInformation = true 34 | } 35 | 36 | buildFeatures { 37 | compose = true 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation(libs.compose.foundation) 43 | implementation(libs.compose.ui) 44 | implementation(libs.compose.runtime) 45 | } -------------------------------------------------------------------------------- /libs/gesture/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /libs/gesture/src/main/java/com/smarttoolfactory/gesture/MotionEvent.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.gesture 2 | 3 | enum class MotionEvent { 4 | Idle, Down, Move, Up 5 | } 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /screenshots/items/community_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/screenshots/items/community_banner.png -------------------------------------------------------------------------------- /screenshots/items/get-it-on-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/screenshots/items/get-it-on-github.png -------------------------------------------------------------------------------- /screenshots/items/support_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/screenshots/items/support_banner.png -------------------------------------------------------------------------------- /screenshots/items/support_paypal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/screenshots/items/support_paypal.png -------------------------------------------------------------------------------- /screenshots/items/support_revolut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/screenshots/items/support_revolut.png -------------------------------------------------------------------------------- /screenshots/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IacobIonut01/Gallery/09d88ec6444cae2219248eb2b950da931bf43de0/screenshots/preview.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | maven("https://jitpack.io") 7 | mavenLocal() 8 | } 9 | } 10 | dependencyResolutionManagement { 11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 12 | repositories { 13 | google() 14 | mavenCentral() 15 | maven("https://jitpack.io") 16 | mavenLocal() 17 | } 18 | } 19 | rootProject.name = "Gallery" 20 | include(":app") 21 | include(":baselineprofile") 22 | include(":libs:gesture") 23 | include(":libs:cropper") -------------------------------------------------------------------------------- /strip_allfiles_permission.sh: -------------------------------------------------------------------------------- 1 | sed -n '//!p' app/src/main/AndroidManifest.xml > app/src/main/AndroidManifest2.xml 2 | mv app/src/main/AndroidManifest2.xml app/src/main/AndroidManifest.xml -------------------------------------------------------------------------------- /strip_permission.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sed -n '//!p' app/src/main/AndroidManifest.xml > app/src/main/AndroidManifest2.xml 3 | mv app/src/main/AndroidManifest2.xml app/src/main/AndroidManifest.xml --------------------------------------------------------------------------------