├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── new-feature.yml │ └── question.yml ├── release.yml └── workflows │ ├── main.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── apollo-compiler ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── java │ └── com │ │ └── github │ │ └── damontecres │ │ └── apollo │ │ └── compiler │ │ ├── StashApolloCompilerPlugin.kt │ │ └── StashApolloCompilerPluginProvider.kt │ └── resources │ └── META-INF │ └── services │ └── com.apollographql.apollo.compiler.ApolloCompilerPluginProvider ├── app ├── .gitignore ├── build.gradle.kts ├── schemas │ └── com.github.damontecres.stashapp.data.room.AppDatabase │ │ ├── 4.json │ │ └── 5.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── github │ │ └── damontecres │ │ └── stashapp │ │ └── TestDbMigrations.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── LICENSE │ │ └── licenses │ │ │ ├── fontawesome.txt │ │ │ ├── glide.txt │ │ │ ├── previewseekbar.txt │ │ │ └── zoomlayout.txt │ ├── graphql │ │ ├── Configuration.graphql │ │ ├── FindGalleries.graphql │ │ ├── FindGroups.graphql │ │ ├── FindImages.graphql │ │ ├── FindPerformers.graphql │ │ ├── FindSavedFilter.graphql │ │ ├── FindSceneMarkers.graphql │ │ ├── FindScenes.graphql │ │ ├── FindStudios.graphql │ │ ├── FindTags.graphql │ │ ├── Fragments.graphql │ │ ├── ImageUpdate.graphql │ │ ├── README.md │ │ ├── RunPluginTask.graphql │ │ ├── SaveSceneActivity.graphql │ │ ├── SceneUpdate.graphql │ │ ├── ServerInfo.graphql │ │ ├── Subscriptions.graphql │ │ └── SystemStatus.graphql │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── damontecres │ │ │ └── stashapp │ │ │ ├── DebugFragment.kt │ │ │ ├── DetailsFragment.kt │ │ │ ├── FilterDebugFragment.kt │ │ │ ├── FilterFragment.kt │ │ │ ├── GalleryDetailsFragment.kt │ │ │ ├── GalleryFragment.kt │ │ │ ├── GroupDetailsFragment.kt │ │ │ ├── GroupFragment.kt │ │ │ ├── LicenseFragment.kt │ │ │ ├── MainFragment.kt │ │ │ ├── MarkerDetailsFragment.kt │ │ │ ├── PerformerDetailsFragment.kt │ │ │ ├── PerformerFragment.kt │ │ │ ├── PinFragment.kt │ │ │ ├── PinFragmentCompose.kt │ │ │ ├── RootActivity.kt │ │ │ ├── SceneDetailsFragment.kt │ │ │ ├── SearchForFragment.kt │ │ │ ├── SettingsFragment.kt │ │ │ ├── SettingsUiFragment.kt │ │ │ ├── StashApplication.kt │ │ │ ├── StashDataGridFragment.kt │ │ │ ├── StashExoPlayer.kt │ │ │ ├── StashGridControlsFragment.kt │ │ │ ├── StashSearchFragment.kt │ │ │ ├── StudioDetailsFragment.kt │ │ │ ├── StudioFragment.kt │ │ │ ├── TabbedFragment.kt │ │ │ ├── TagDetailsFragment.kt │ │ │ ├── TagFragment.kt │ │ │ ├── UpdateAppFragment.kt │ │ │ ├── UpdateChangelogFragment.kt │ │ │ ├── actions │ │ │ ├── CreateMarkerAction.kt │ │ │ ├── StashAction.kt │ │ │ └── StashActionClickedListener.kt │ │ │ ├── api │ │ │ └── fragment │ │ │ │ ├── GroupRelationshipData.kt │ │ │ │ └── StashData.kt │ │ │ ├── data │ │ │ ├── DataType.kt │ │ │ ├── JobResult.kt │ │ │ ├── OCounter.kt │ │ │ ├── PlaylistItem.kt │ │ │ ├── Scene.kt │ │ │ ├── SortAndDirection.kt │ │ │ ├── SortOption.kt │ │ │ ├── StashFindFilter.kt │ │ │ ├── ThrottledLiveData.kt │ │ │ ├── VideoFilter.kt │ │ │ └── room │ │ │ │ ├── AppDatabase.kt │ │ │ │ ├── Converters.kt │ │ │ │ ├── Migrations.kt │ │ │ │ ├── PlaybackEffect.kt │ │ │ │ ├── PlaybackEffectsDao.kt │ │ │ │ ├── RecentSearchItem.kt │ │ │ │ └── RecentSearchItemsDao.kt │ │ │ ├── filter │ │ │ ├── CreateFilterFragment.kt │ │ │ ├── CreateFilterGuidedStepFragment.kt │ │ │ ├── CreateFilterStep.kt │ │ │ ├── CreateFilterViewModel.kt │ │ │ ├── CreateFindFilterFragment.kt │ │ │ ├── CreateObjectFilterStep.kt │ │ │ ├── DescriptionExtractors.kt │ │ │ ├── FilterOption.kt │ │ │ ├── StashGuidedActionsStylist.kt │ │ │ ├── output │ │ │ │ ├── FilterOutputs.kt │ │ │ │ └── FilterWriter.kt │ │ │ └── picker │ │ │ │ ├── BooleanPickerFragment.kt │ │ │ │ ├── CircumcisionPickerFragment.kt │ │ │ │ ├── DatePickerFragment.kt │ │ │ │ ├── DurationPickerFragment.kt │ │ │ │ ├── FloatPickerFragment.kt │ │ │ │ ├── GenderPickerFragment.kt │ │ │ │ ├── GuidedDurationPickerAction.kt │ │ │ │ ├── HierarchicalMultiCriterionFragment.kt │ │ │ │ ├── IntPickerFragment.kt │ │ │ │ ├── MultiCriterionFragment.kt │ │ │ │ ├── OrientationPickerFragment.kt │ │ │ │ ├── RatingPickerFragment.kt │ │ │ │ ├── ResolutionPickerFragment.kt │ │ │ │ ├── SearchPickerFragment.kt │ │ │ │ ├── StringPickerFragment.kt │ │ │ │ └── TwoValuePicker.kt │ │ │ ├── image │ │ │ ├── EffectTransformation.kt │ │ │ ├── ImageClipFragment.kt │ │ │ ├── ImageController.kt │ │ │ ├── ImageDetailsFragment.kt │ │ │ ├── ImageFragment.kt │ │ │ └── ImageViewFragment.kt │ │ │ ├── navigation │ │ │ ├── Destination.kt │ │ │ ├── FilterAndPosition.kt │ │ │ ├── NavigationManager.kt │ │ │ ├── NavigationManagerCompose.kt │ │ │ └── NavigationOnItemViewClickedListener.kt │ │ │ ├── playback │ │ │ ├── CodecSupport.kt │ │ │ ├── ControllerVisibilityListenerList.kt │ │ │ ├── PlaybackFragment.kt │ │ │ ├── PlaybackSceneFragment.kt │ │ │ ├── PlaybackVideoFiltersFragment.kt │ │ │ ├── PlaylistFragment.kt │ │ │ ├── PlaylistListFragment.kt │ │ │ ├── PlaylistMarkersFragment.kt │ │ │ ├── PlaylistScenesFragment.kt │ │ │ ├── PlaylistViewModel.kt │ │ │ ├── StashPlayerView.kt │ │ │ ├── StreamUtils.kt │ │ │ ├── TrackActivityPlaybackListener.kt │ │ │ └── VideoFilterViewModel.kt │ │ │ ├── presenters │ │ │ ├── ActionPresenter.kt │ │ │ ├── ClassPresenterSelector.kt │ │ │ ├── CreateMarkerActionPresenter.kt │ │ │ ├── FilterArgsPresenter.kt │ │ │ ├── GalleryPresenter.kt │ │ │ ├── GroupPresenter.kt │ │ │ ├── GroupRelationshipPresenter.kt │ │ │ ├── ImagePresenter.kt │ │ │ ├── MarkerPresenter.kt │ │ │ ├── NullPresenter.kt │ │ │ ├── NullPresenterSelector.kt │ │ │ ├── OCounterPresenter.kt │ │ │ ├── PerformerInScenePresenter.kt │ │ │ ├── PerformerPresenter.kt │ │ │ ├── PlaylistItemPresenter.kt │ │ │ ├── PopupOnLongClickListener.kt │ │ │ ├── SceneDetailsPresenter.kt │ │ │ ├── ScenePresenter.kt │ │ │ ├── StashImageCardView.kt │ │ │ ├── StashPresenter.kt │ │ │ ├── StudioPresenter.kt │ │ │ └── TagPresenter.kt │ │ │ ├── setup │ │ │ ├── ConfigureServerStep.kt │ │ │ ├── ManageServersFragment.kt │ │ │ ├── SetupFragment.kt │ │ │ ├── SetupGuidedStepSupportFragment.kt │ │ │ ├── SetupState.kt │ │ │ ├── SetupStep1ServerUrl.kt │ │ │ ├── SetupStep2Ssl.kt │ │ │ ├── SetupStep3ApiKey.kt │ │ │ ├── SetupStep4Pin.kt │ │ │ └── readonly │ │ │ │ ├── ReadOnlyPinConfigFragment.kt │ │ │ │ └── SettingsPinEntryFragment.kt │ │ │ ├── suppliers │ │ │ ├── DataSupplierFactory.kt │ │ │ ├── FilterArgs.kt │ │ │ ├── GalleryDataSupplier.kt │ │ │ ├── GroupDataSupplier.kt │ │ │ ├── GroupRelationshipDataSupplier.kt │ │ │ ├── ImageDataSupplier.kt │ │ │ ├── MarkerDataSupplier.kt │ │ │ ├── PerformerDataSupplier.kt │ │ │ ├── PerformerTagDataSupplier.kt │ │ │ ├── SceneDataSupplier.kt │ │ │ ├── StashPagingSource.kt │ │ │ ├── StashSparseFilterFetcher.kt │ │ │ ├── StudioDataSupplier.kt │ │ │ ├── TagDataSupplier.kt │ │ │ └── VideoSceneDataSupplier.kt │ │ │ ├── ui │ │ │ ├── ComposeUiConfig.kt │ │ │ ├── Extensions.kt │ │ │ ├── FilterViewModel.kt │ │ │ ├── NavDrawerFragment.kt │ │ │ ├── PreviewUtils.kt │ │ │ ├── Themes.kt │ │ │ ├── cards │ │ │ │ ├── Cards.kt │ │ │ │ ├── FilterCards.kt │ │ │ │ ├── GalleryCard.kt │ │ │ │ ├── GroupCard.kt │ │ │ │ ├── ImageCard.kt │ │ │ │ ├── MarkerCard.kt │ │ │ │ ├── PerformerCard.kt │ │ │ │ ├── SceneCard.kt │ │ │ │ ├── StudioCard.kt │ │ │ │ └── TagCard.kt │ │ │ ├── components │ │ │ │ ├── CircularProgress.kt │ │ │ │ ├── Dialogs.kt │ │ │ │ ├── DotSeparatedRow.kt │ │ │ │ ├── EditTextBox.kt │ │ │ │ ├── ItemDetails.kt │ │ │ │ ├── ItemOnClicker.kt │ │ │ │ ├── ItemsRow.kt │ │ │ │ ├── LongClicker.kt │ │ │ │ ├── Rating.kt │ │ │ │ ├── SavedFiltersButton.kt │ │ │ │ ├── SliderBar.kt │ │ │ │ ├── SortByButton.kt │ │ │ │ ├── StashGrid.kt │ │ │ │ ├── SwitchWithLabel.kt │ │ │ │ ├── TabPage.kt │ │ │ │ ├── TableRow.kt │ │ │ │ ├── TitleValueText.kt │ │ │ │ ├── image │ │ │ │ │ ├── ImageControlsOverlay.kt │ │ │ │ │ ├── ImageDetailsFooter.kt │ │ │ │ │ ├── ImageDetailsHeader.kt │ │ │ │ │ ├── ImageDetailsViewModel.kt │ │ │ │ │ ├── ImageFilterSliders.kt │ │ │ │ │ └── ImageOverlay.kt │ │ │ │ ├── main │ │ │ │ │ ├── MainPageHeader.kt │ │ │ │ │ ├── MainPagePerformerDetails.kt │ │ │ │ │ └── MainPageSceneDetails.kt │ │ │ │ ├── playback │ │ │ │ │ ├── AmbientPlayerListener.kt │ │ │ │ │ ├── KeyIdentifier.kt │ │ │ │ │ ├── PlaybackControls.kt │ │ │ │ │ ├── PlaybackDebugInfo.kt │ │ │ │ │ ├── PlaybackOverlay.kt │ │ │ │ │ ├── PlaybackPageContent.kt │ │ │ │ │ ├── PlaybackState.kt │ │ │ │ │ ├── PlayerControls.kt │ │ │ │ │ ├── PlaylistList.kt │ │ │ │ │ ├── PlaylistListDialog.kt │ │ │ │ │ ├── SceneDetailsOverlay.kt │ │ │ │ │ ├── SeekBar.kt │ │ │ │ │ ├── SeekBarState.kt │ │ │ │ │ └── SkipIndicator.kt │ │ │ │ └── scene │ │ │ │ │ ├── SceneDetailsBody.kt │ │ │ │ │ ├── SceneDetailsFooter.kt │ │ │ │ │ ├── SceneDetailsHeader.kt │ │ │ │ │ ├── SceneDetailsViewModel.kt │ │ │ │ │ └── ScenePlayButton.kt │ │ │ ├── pages │ │ │ │ ├── ChooseThemePage.kt │ │ │ │ ├── FilterPage.kt │ │ │ │ ├── GalleryPage.kt │ │ │ │ ├── GroupPage.kt │ │ │ │ ├── ImagePage.kt │ │ │ │ ├── MainPage.kt │ │ │ │ ├── MarkerPage.kt │ │ │ │ ├── PerformerPage.kt │ │ │ │ ├── PinEntryPage.kt │ │ │ │ ├── PlaybackPage.kt │ │ │ │ ├── SceneDetailsPage.kt │ │ │ │ ├── SearchForPage.kt │ │ │ │ ├── SearchPage.kt │ │ │ │ ├── StudioPage.kt │ │ │ │ └── TagPage.kt │ │ │ ├── theme │ │ │ │ ├── Color.kt │ │ │ │ └── Theme.kt │ │ │ └── util │ │ │ │ ├── CoilPreviewTransformation.kt │ │ │ │ ├── CrossFadeFactory.kt │ │ │ │ ├── ModifierUtils.kt │ │ │ │ └── OneTimeLaunchedEffect.kt │ │ │ ├── util │ │ │ ├── AlphabetSearchUtils.kt │ │ │ ├── AndroidExtensions.kt │ │ │ ├── AppUpgradeHandler.kt │ │ │ ├── Comparators.kt │ │ │ ├── ComposePager.kt │ │ │ ├── Constants.kt │ │ │ ├── CreateNew.kt │ │ │ ├── DefaultKeyEventCallback.kt │ │ │ ├── FilterParser.kt │ │ │ ├── FilterUtils.kt │ │ │ ├── FrontPageParser.kt │ │ │ ├── KeyEventDispatcher.kt │ │ │ ├── ListRowManager.kt │ │ │ ├── LongClickPreference.kt │ │ │ ├── MappedList.kt │ │ │ ├── MutationEngine.kt │ │ │ ├── OCounterLongClickCallBack.kt │ │ │ ├── OptionalSerializer.kt │ │ │ ├── PageFilterKey.kt │ │ │ ├── PagingObjectAdapter.kt │ │ │ ├── QueryEngine.kt │ │ │ ├── RemoveLongClickListener.kt │ │ │ ├── ServerPreferences.kt │ │ │ ├── SingleItemObjectAdapter.kt │ │ │ ├── SkipParams.kt │ │ │ ├── StashClient.kt │ │ │ ├── StashCoroutineExceptionHandler.kt │ │ │ ├── StashEngine.kt │ │ │ ├── StashFragmentPagerAdapter.kt │ │ │ ├── StashGlide.kt │ │ │ ├── StashGlideModule.kt │ │ │ ├── StashPreviewLoader.kt │ │ │ ├── StashServer.kt │ │ │ ├── SubscriptionEngine.kt │ │ │ ├── UpdateChecker.kt │ │ │ ├── Version.kt │ │ │ ├── plugin │ │ │ │ ├── CompanionPlugin.kt │ │ │ │ ├── CrashReportSenderFactory.kt │ │ │ │ └── LogcatTags.kt │ │ │ └── svg │ │ │ │ ├── SvgDecoder.kt │ │ │ │ ├── SvgDrawableTranscoder.kt │ │ │ │ └── SvgSoftwareLayerSetter.kt │ │ │ └── views │ │ │ ├── ClassOnItemViewClickedListener.kt │ │ │ ├── DurationPicker.kt │ │ │ ├── DurationPicker2.kt │ │ │ ├── FontSpan.kt │ │ │ ├── Formatting.kt │ │ │ ├── HomeImageButton.kt │ │ │ ├── LoadingFragment.kt │ │ │ ├── MainTitleView.kt │ │ │ ├── MarkerPickerFragment.kt │ │ │ ├── PlayAllOnClickListener.kt │ │ │ ├── SimpleListPopupWindow.kt │ │ │ ├── SkipIndicator.kt │ │ │ ├── SortButtonManager.kt │ │ │ ├── StarRatingBar.kt │ │ │ ├── StashOnFocusChangeListener.kt │ │ │ ├── StashRatingBar.kt │ │ │ ├── StashZoomImageView.kt │ │ │ ├── TabbedGridTitleView.kt │ │ │ ├── TitleTransitionHelper.kt │ │ │ ├── WrapAroundSeekBar.kt │ │ │ ├── dialog │ │ │ └── ConfirmationDialogFragment.kt │ │ │ └── models │ │ │ ├── CardUiSettings.kt │ │ │ ├── EqualityMutableLiveData.kt │ │ │ ├── GalleryViewModel.kt │ │ │ ├── GroupViewModel.kt │ │ │ ├── ImageViewModel.kt │ │ │ ├── ItemViewModel.kt │ │ │ ├── MarkerDetailsViewModel.kt │ │ │ ├── PerformerViewModel.kt │ │ │ ├── PlaybackViewModel.kt │ │ │ ├── SceneViewModel.kt │ │ │ ├── ServerViewModel.kt │ │ │ ├── StashGridViewModel.kt │ │ │ ├── StudioViewModel.kt │ │ │ ├── TabbedGridViewModel.kt │ │ │ └── TagViewModel.kt │ └── res │ │ ├── anim │ │ ├── slide_in_right.xml │ │ └── slide_out_left.xml │ │ ├── animator │ │ ├── fade_in.xml │ │ └── fade_out.xml │ │ ├── color │ │ ├── clickable_text.xml │ │ ├── filter_thumb.xml │ │ ├── seek_bar_blue.xml │ │ ├── seek_bar_green.xml │ │ └── seek_bar_red.xml │ │ ├── drawable │ │ ├── baseline_add_box_24.xml │ │ ├── baseline_camera_indoor_48.xml │ │ ├── baseline_fast_forward_24.xml │ │ ├── baseline_fast_rewind_24.xml │ │ ├── baseline_more_vert_96.xml │ │ ├── baseline_pause_24.xml │ │ ├── baseline_play_arrow_24.xml │ │ ├── baseline_skip_next_24.xml │ │ ├── baseline_skip_previous_24.xml │ │ ├── baseline_stop_24.xml │ │ ├── baseline_undo_24.xml │ │ ├── button_selector.xml │ │ ├── button_selector_default_bg.xml │ │ ├── button_selector_marker_picker.xml │ │ ├── captions_svgrepo_com.xml │ │ ├── circular_arrow_right.xml │ │ ├── default_gallery.xml │ │ ├── default_group.xml │ │ ├── default_image.xml │ │ ├── default_scene.xml │ │ ├── default_studio.xml │ │ ├── default_tag.xml │ │ ├── favorite_button_selector.xml │ │ ├── guided_actions_selector.xml │ │ ├── icon_button_selector.xml │ │ ├── image_button_selector.xml │ │ ├── playback_button_selector.xml │ │ ├── playback_popup_item_background.xml │ │ ├── popup_item_background.xml │ │ ├── rectangle.xml │ │ ├── scrollbar_thumb.xml │ │ ├── search_cursor.xml │ │ ├── selected_rectangle.xml │ │ ├── sweat_drops.xml │ │ ├── transparent_button_selector.xml │ │ ├── vector_settings.xml │ │ └── video_frame.xml │ │ ├── font │ │ └── fa_solid_900.ttf │ │ ├── layout │ │ ├── activity_root.xml │ │ ├── activity_root_compose.xml │ │ ├── activity_root_pin.xml │ │ ├── alphabet_button.xml │ │ ├── apply_video_filters.xml │ │ ├── changelog.xml │ │ ├── compose_frame.xml │ │ ├── debug.xml │ │ ├── debug_supported_row.xml │ │ ├── details_view.xml │ │ ├── duration_guided_action.xml │ │ ├── exo_player_control_view.xml │ │ ├── filter_debug.xml │ │ ├── filter_debug_page.xml │ │ ├── filter_debug_page_row.xml │ │ ├── filter_list.xml │ │ ├── frame.xml │ │ ├── grid_footer_layout.xml │ │ ├── group_view.xml │ │ ├── image_action_button.xml │ │ ├── image_card_extra_content_row.xml │ │ ├── image_card_icon_row.xml │ │ ├── image_clip_playback.xml │ │ ├── image_fragment.xml │ │ ├── image_layout.xml │ │ ├── jump_buttons.xml │ │ ├── lb_details_description.xml │ │ ├── lb_image_card_view.xml │ │ ├── license.xml │ │ ├── loading_fragment.xml │ │ ├── main_title_view.xml │ │ ├── marker_picker.xml │ │ ├── performer_details_compose.xml │ │ ├── pin_dialog.xml │ │ ├── playlist_item.xml │ │ ├── playlist_list.xml │ │ ├── popup_header.xml │ │ ├── popup_item.xml │ │ ├── root_fragment_layout.xml │ │ ├── skip_indicator.xml │ │ ├── sort_popup_item.xml │ │ ├── stash_card_player_view.xml │ │ ├── stash_grid.xml │ │ ├── stash_grid_controls.xml │ │ ├── stash_rating_bar.xml │ │ ├── tabbed_grid_view.xml │ │ ├── table_row.xml │ │ ├── title.xml │ │ └── video_playback.xml │ │ ├── mipmap-nodpi │ │ └── stash_logo.png │ │ ├── values-night │ │ └── styles.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── attrs_main_title_view.xml │ │ ├── attrs_rating_bar.xml │ │ ├── colors.xml │ │ ├── create_filter_strings.xml │ │ ├── dimens.xml │ │ ├── license.xml │ │ ├── preferences.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ ├── themes.xml │ │ └── values.xml │ │ └── xml │ │ ├── advanced_preferences.xml │ │ ├── provider_paths.xml │ │ ├── root_preferences.xml │ │ └── ui_preferences.xml │ └── test │ ├── java │ ├── android │ │ └── util │ │ │ └── Log.kt │ └── com │ │ └── github │ │ └── damontecres │ │ └── stashapp │ │ ├── FormattingTests.kt │ │ ├── FrontPageFilterTests.kt │ │ ├── ObjectFilterParsingTests.kt │ │ ├── SortOptionTests.kt │ │ └── VersionCompareTests.kt │ └── resources │ ├── front_page_basic.json │ ├── front_page_unsupported.json │ ├── gender_savedfilter.json │ ├── image_savedfilter.json │ ├── performer_custom_fields.json │ ├── performer_savedfilter.json │ ├── scene_savedfilter.json │ ├── scene_savedfilter2.json │ ├── studio_children_savedfilter.json │ └── tag_savedfilter.json ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── java │ └── com │ └── github │ └── damontecres │ └── buildsrc │ └── ParseStashStrings.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.kt] 4 | ktlint_function_naming_ignore_when_annotated_with=Composable 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: "Bug Report" 2 | description: Report a bug 3 | title: "[BUG] - " 4 | labels: [ 5 | "bug" 6 | ] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: "Description" 12 | description: Please enter a description of the bug 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: reprod 17 | attributes: 18 | label: "Reproduction steps" 19 | description: Please enter the steps to reproduce 20 | value: | 21 | 1. Click on '...' 22 | 2. Scroll to '....' 23 | 3. See error 24 | validations: 25 | required: false 26 | - type: input 27 | id: app-version 28 | attributes: 29 | label: "App Version" 30 | description: What version of the app? 31 | placeholder: "v0.5.0" 32 | validations: 33 | required: true 34 | - type: input 35 | id: server 36 | attributes: 37 | label: "Server Version" 38 | description: What version of the server? 39 | placeholder: "v0.27.2" 40 | validations: 41 | required: true 42 | - type: input 43 | id: device 44 | attributes: 45 | label: "Device" 46 | description: What device are you using? 47 | placeholder: "NVIDIA Shield TV Pro 2019" 48 | validations: 49 | required: false 50 | - type: textarea 51 | id: logs 52 | attributes: 53 | label: "Logs" 54 | description: Please [gather any logs or crash reports](https://github.com/damontecres/StashAppAndroidTV/wiki/Gathering-app-logs) and include them here. 55 | placeholder: Review logs for personal information before sharing them here publicly! 56 | render: bash 57 | validations: 58 | required: false 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-feature.yml: -------------------------------------------------------------------------------- 1 | name: "Feature Request" 2 | description: Request a new feature 3 | title: "[FEA] - <title>" 4 | labels: [ 5 | "enhancement" 6 | ] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: "Description" 12 | description: Please enter a description of the feature 13 | placeholder: "I would like to see..." 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: "Question" 2 | description: Ask a question 3 | title: "[QST] - <title>" 4 | labels: [ 5 | "question" 6 | ] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: "Question" 12 | description: Please enter your question 13 | placeholder: "How do I...?" 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: [] 4 | authors: [] 5 | categories: 6 | - title: Breaking Changes 7 | labels: 8 | - breaking change 9 | - title: New Features & Enhancements 10 | labels: 11 | - enhancement 12 | - title: UI/UX Improvements 13 | labels: 14 | - user interface 15 | exclude: 16 | labels: 17 | - bug 18 | - new ui 19 | - title: New UI/UX Changes 20 | labels: 21 | - new ui 22 | - title: Bug Fixes 23 | labels: 24 | - bug 25 | - title: Other changes 26 | labels: 27 | - "*" 28 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | 6 | defaults: 7 | run: 8 | shell: bash 9 | 10 | env: 11 | BUILD_TOOLS_VERSION: 35.0.0 12 | BUILD_DIRS_ARTIFACT: build-dirs 13 | 14 | jobs: 15 | pre-commit: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout the code 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 # Need the tags to build 22 | submodules: true # Need the submodules to build 23 | - name: Setup Python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: '3.10' 27 | - name: Run pre-commit 28 | uses: pre-commit/action@v3.0.1 29 | 30 | build: 31 | runs-on: ubuntu-latest 32 | needs: pre-commit 33 | steps: 34 | - name: Checkout the code 35 | uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 # Need the tags to build 38 | submodules: true # Need the submodules to build 39 | - name: Setup JDK 40 | uses: actions/setup-java@v4 41 | with: 42 | distribution: zulu 43 | java-version: '21' 44 | cache: 'gradle' 45 | - name: Setup Android SDK 46 | uses: android-actions/setup-android@v3 47 | with: 48 | packages: "tools platform-tools build-tools;${{ env.BUILD_TOOLS_VERSION }}" 49 | - name: Build app 50 | id: buildapp 51 | run: | 52 | ./gradlew clean build 53 | echo "apk=$(ls app/build/outputs/apk/debug/StashAppAndroidTV-debug*.apk)" >> "$GITHUB_OUTPUT" 54 | - name: Tar build dirs 55 | run: | 56 | tar -czf build.tgz */build/. 57 | - uses: actions/upload-artifact@v4 58 | id: upload-build-dirs 59 | with: 60 | name: "${{ env.BUILD_DIRS_ARTIFACT }}" 61 | path: build.tgz 62 | if-no-files-found: error 63 | - uses: actions/upload-artifact@v4 64 | with: 65 | name: StashAppAndroidTV-debug.apk 66 | path: "${{ steps.buildapp.outputs.apk }}" 67 | compression-level: 0 68 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | env: 13 | APK_SHORT_NAME: StashAppAndroidTV.apk 14 | BUILD_TOOLS_VERSION: 35.0.0 15 | 16 | jobs: 17 | publish: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: write 21 | steps: 22 | - name: Checkout the code 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 # Need the tags to build 26 | submodules: true # Need the submodules to build 27 | - name: Setup Python 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: '3.10' 31 | - name: Setup JDK 32 | uses: actions/setup-java@v4 33 | with: 34 | distribution: zulu 35 | java-version: '21' 36 | cache: 'gradle' 37 | - name: Setup Android SDK 38 | uses: android-actions/setup-android@v3 39 | with: 40 | packages: "tools platform-tools build-tools;${{ env.BUILD_TOOLS_VERSION }}" 41 | - name: Build app 42 | id: buildapp 43 | env: 44 | KEY_ALIAS: "${{ secrets.KEY_ALIAS }}" 45 | KEY_PASSWORD: "${{ secrets.KEY_PASSWORD }}" 46 | KEY_STORE_PASSWORD: "${{ secrets.KEY_STORE_PASSWORD }}" 47 | SIGNING_KEY: "${{ secrets.SIGNING_KEY }}" 48 | run: | 49 | ./gradlew clean assembleRelease 50 | echo "apk=$(ls app/build/outputs/apk/release/StashAppAndroidTV-release*.apk)" >> "$GITHUB_OUTPUT" 51 | - name: Verify signature 52 | run: | 53 | ${{env.ANDROID_SDK_ROOT}}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify --verbose --print-certs "${{ steps.buildapp.outputs.apk }}" 54 | - name: Copy APK to ${{ env.APK_SHORT_NAME }} 55 | run: | 56 | cp ${{ steps.buildapp.outputs.apk }} ${{ env.APK_SHORT_NAME }} 57 | - name: Create GitHub release 58 | uses: ncipollo/release-action@v1 59 | with: 60 | artifacts: "${{ steps.buildapp.outputs.apk }},${{ env.APK_SHORT_NAME }}" 61 | makeLatest: true 62 | prerelease: false 63 | generateReleaseNotes: true 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Hardcoded crentials 2 | StashCredentials.kt 3 | stash_strings.xml 4 | test-server/ 5 | 6 | # Gradle files 7 | .gradle/ 8 | build/ 9 | 10 | # Local configuration file (sdk path, etc) 11 | local.properties 12 | 13 | # Log/OS Files 14 | *.log 15 | 16 | # Android Studio generated files and folders 17 | captures/ 18 | .externalNativeBuild/ 19 | .cxx/ 20 | *.apk 21 | output.json 22 | 23 | # IntelliJ 24 | *.iml 25 | .idea/ 26 | misc.xml 27 | deploymentTargetDropDown.xml 28 | render.experimental.xml 29 | 30 | # Keystore files 31 | *.jks 32 | *.keystore 33 | 34 | # Google Services (e.g. APIs or Firebase) 35 | google-services.json 36 | 37 | # Android Profiling 38 | *.hprof 39 | 40 | # Android Studio generated 41 | .gradle 42 | /local.properties 43 | /.idea/caches 44 | /.idea/libraries 45 | /.idea/modules.xml 46 | /.idea/workspace.xml 47 | /.idea/navEditor.xml 48 | /.idea/assetWizardSettings.xml 49 | .DS_Store 50 | /build 51 | /captures 52 | .externalNativeBuild 53 | .cxx 54 | 55 | .kotlin/ 56 | 57 | schema.graphqls 58 | 59 | # Local scripts 60 | local_scripts/ 61 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "stash"] 2 | path = stash-server 3 | url = https://github.com/stashapp/stash.git 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-xml 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 10 | rev: v2.12.0 11 | hooks: 12 | - id: pretty-format-kotlin 13 | args: [ --autofix, --ktlint-version=1.4.1 ] 14 | - repo: local 15 | hooks: 16 | - id: check-debug-enabled 17 | name: Check debug enabled 18 | entry: val DEBUG = true 19 | language: pygrep 20 | files: '.+\.kt' 21 | -------------------------------------------------------------------------------- /apollo-compiler/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /apollo-compiler/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | alias(libs.plugins.kotlin.jvm) 4 | } 5 | 6 | java { 7 | sourceCompatibility = JavaVersion.VERSION_21 8 | targetCompatibility = JavaVersion.VERSION_21 9 | } 10 | 11 | dependencies { 12 | implementation(libs.apollo.compiler) 13 | api(libs.kotlinx.serialization.core) 14 | } 15 | -------------------------------------------------------------------------------- /apollo-compiler/src/main/java/com/github/damontecres/apollo/compiler/StashApolloCompilerPluginProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.apollo.compiler 2 | 3 | import com.apollographql.apollo.annotations.ApolloExperimental 4 | import com.apollographql.apollo.compiler.ApolloCompilerPlugin 5 | import com.apollographql.apollo.compiler.ApolloCompilerPluginEnvironment 6 | import com.apollographql.apollo.compiler.ApolloCompilerPluginProvider 7 | 8 | @OptIn(ApolloExperimental::class) 9 | class StashApolloCompilerPluginProvider : ApolloCompilerPluginProvider { 10 | override fun create(environment: ApolloCompilerPluginEnvironment): ApolloCompilerPlugin = StashApolloCompilerPlugin() 11 | } 12 | -------------------------------------------------------------------------------- /apollo-compiler/src/main/resources/META-INF/services/com.apollographql.apollo.compiler.ApolloCompilerPluginProvider: -------------------------------------------------------------------------------- 1 | com.github.damontecres.apollo.compiler.StashApolloCompilerPluginProvider 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/src/main/assets/LICENSE: -------------------------------------------------------------------------------- 1 | ../../../../LICENSE -------------------------------------------------------------------------------- /app/src/main/assets/licenses/previewseekbar.txt: -------------------------------------------------------------------------------- 1 | PreviewSeekBar included from https://github.com/rubensousa/PreviewSeekBar 2 | 3 | Copyright 2017 The Android Open Source Project 4 | Copyright 2020 Rúben Sousa 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | -------------------------------------------------------------------------------- /app/src/main/assets/licenses/zoomlayout.txt: -------------------------------------------------------------------------------- 1 | ZoomLayout included from https://natario1.github.io/ZoomLayout/home 2 | 3 | Copyright (C) 2020 Mattia Iavarone 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /app/src/main/graphql/Configuration.graphql: -------------------------------------------------------------------------------- 1 | query Configuration { 2 | configuration { 3 | defaults{ 4 | scan{ 5 | scanGenerateClipPreviews 6 | scanGenerateCovers 7 | scanGenerateImagePreviews 8 | scanGeneratePhashes 9 | scanGeneratePreviews 10 | scanGenerateSprites 11 | scanGenerateThumbnails 12 | } 13 | generate{ 14 | clipPreviews 15 | covers 16 | imagePreviews 17 | interactiveHeatmapsSpeeds 18 | markerImagePreviews 19 | markers 20 | markerScreenshots 21 | phashes 22 | previews 23 | sprites 24 | transcodes 25 | imageThumbnails 26 | } 27 | } 28 | interface { 29 | menuItems 30 | showStudioAsText 31 | } 32 | ui 33 | } 34 | version { 35 | version 36 | hash 37 | build_time 38 | } 39 | plugins { 40 | id 41 | name 42 | version 43 | } 44 | } 45 | 46 | query ConfigurationUI { 47 | configuration { 48 | ui 49 | } 50 | } 51 | 52 | mutation MetadataScan($input: ScanMetadataInput!){ 53 | metadataScan(input: $input) 54 | } 55 | 56 | mutation MetadataGenerate($input: GenerateMetadataInput!){ 57 | metadataGenerate(input: $input) 58 | } 59 | 60 | mutation InstallPackages($type: PackageType!, $packages: [PackageSpecInput!]!) { 61 | installPackages(type: $type, packages: $packages) 62 | } 63 | 64 | query FindJob($input: FindJobInput!) { 65 | findJob(input: $input) { 66 | ...StashJob 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/graphql/FindGalleries.graphql: -------------------------------------------------------------------------------- 1 | query FindGalleries($filter: FindFilterType, $gallery_filter: GalleryFilterType, $ids: [ID!]) { 2 | findGalleries(filter: $filter, gallery_filter: $gallery_filter, ids: $ids) { 3 | galleries { 4 | ...GalleryData 5 | } 6 | } 7 | } 8 | 9 | query GetGallery($id: ID!) { 10 | findGallery(id: $id) { 11 | ...GalleryData 12 | } 13 | } 14 | 15 | query CountGalleries($filter: FindFilterType, $gallery_filter: GalleryFilterType) { 16 | findGalleries(filter: $filter, gallery_filter: $gallery_filter) { 17 | count 18 | } 19 | } 20 | 21 | query FindGalleryPerformers($id: ID!) { 22 | findGallery(id: $id) { 23 | id 24 | performers { 25 | ...PerformerData 26 | } 27 | } 28 | } 29 | 30 | query FindGalleryTags($id: ID!) { 31 | findGallery(id: $id) { 32 | id 33 | tags { 34 | ...TagData 35 | } 36 | } 37 | } 38 | 39 | fragment GalleryData on Gallery { 40 | id 41 | title 42 | image_count 43 | date 44 | details 45 | rating100 46 | code 47 | photographer 48 | created_at 49 | updated_at 50 | files { 51 | path 52 | } 53 | folder { 54 | path 55 | } 56 | performers { 57 | ...SlimPerformerData 58 | } 59 | tags { 60 | ...SlimTagData 61 | } 62 | studio { 63 | id 64 | name 65 | image_path 66 | } 67 | scenes { 68 | id 69 | } 70 | paths { 71 | cover 72 | preview 73 | } 74 | } 75 | 76 | mutation UpdateGallery($input: GalleryUpdateInput!) { 77 | galleryUpdate(input: $input) { 78 | ...GalleryData 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/graphql/FindImages.graphql: -------------------------------------------------------------------------------- 1 | query FindImages($filter: FindFilterType, $image_filter: ImageFilterType, $ids: [ID!]) { 2 | findImages(filter: $filter, image_filter: $image_filter, ids: $ids) { 3 | images { 4 | ...ImageData 5 | } 6 | } 7 | } 8 | 9 | query CountImages($filter: FindFilterType, $image_filter: ImageFilterType) { 10 | findImages(filter: $filter, image_filter: $image_filter) { 11 | count 12 | } 13 | } 14 | 15 | query FindImage($id: ID) { 16 | findImage(id: $id) { 17 | ...ImageData 18 | } 19 | } 20 | 21 | query GetExtraImage($id: ID) { 22 | findImage(id: $id) { 23 | ...ExtraImageData 24 | } 25 | } 26 | 27 | fragment ImageData on Image { 28 | id 29 | title 30 | code 31 | rating100 32 | date 33 | details 34 | photographer 35 | o_counter 36 | created_at 37 | updated_at 38 | paths { 39 | thumbnail 40 | preview 41 | image 42 | } 43 | performers{ 44 | id 45 | } 46 | studio { 47 | id 48 | name 49 | } 50 | tags { 51 | id 52 | } 53 | galleries { 54 | id 55 | } 56 | visual_files { 57 | ... on BaseFile{ 58 | id 59 | path 60 | size 61 | __typename 62 | } 63 | ... on ImageFile { 64 | width 65 | height 66 | 67 | } 68 | ... on VideoFile{ 69 | width 70 | height 71 | format 72 | video_codec 73 | audio_codec 74 | duration 75 | } 76 | } 77 | } 78 | 79 | fragment ExtraImageData on Image { 80 | id 81 | performers{ 82 | ...PerformerData 83 | } 84 | tags { 85 | ...TagData 86 | } 87 | galleries { 88 | ...GalleryData 89 | } 90 | studio { 91 | ...StudioData 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/graphql/FindSavedFilter.graphql: -------------------------------------------------------------------------------- 1 | query FindSavedFilter($id: ID!) { 2 | findSavedFilter(id: $id) { 3 | ...SavedFilter 4 | __typename 5 | } 6 | } 7 | 8 | query FindSavedFilters($mode: FilterMode!) { 9 | findSavedFilters(mode: $mode) { 10 | ...SavedFilter 11 | __typename 12 | } 13 | } 14 | 15 | fragment SavedFilter on SavedFilter { 16 | id 17 | mode 18 | name 19 | find_filter { 20 | q 21 | page 22 | per_page 23 | sort 24 | direction 25 | __typename 26 | } 27 | object_filter 28 | ui_options 29 | __typename 30 | } 31 | 32 | mutation SaveFilter($input: SaveFilterInput!) { 33 | saveFilter(input: $input) { 34 | ...SavedFilter 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/graphql/FindStudios.graphql: -------------------------------------------------------------------------------- 1 | query FindStudios($filter: FindFilterType, $studio_filter: StudioFilterType, $ids: [ID!]) { 2 | findStudios(filter: $filter, studio_filter: $studio_filter, ids: $ids) { 3 | studios { 4 | ...StudioData 5 | __typename 6 | } 7 | __typename 8 | } 9 | } 10 | 11 | query GetStudio($id: ID!) { 12 | findStudio(id: $id) { 13 | ...StudioData 14 | } 15 | } 16 | 17 | query CountStudios($filter: FindFilterType, $studio_filter: StudioFilterType) { 18 | findStudios(filter: $filter, studio_filter: $studio_filter) { 19 | count 20 | } 21 | } 22 | 23 | query FindStudioTags($id: ID!) { 24 | findStudio(id: $id) { 25 | id 26 | tags { 27 | ...TagData 28 | } 29 | } 30 | } 31 | 32 | fragment StudioData on Studio { 33 | id 34 | name 35 | url 36 | parent_studio { 37 | id 38 | name 39 | url 40 | image_path 41 | __typename 42 | } 43 | child_studios { 44 | id 45 | name 46 | image_path 47 | __typename 48 | } 49 | tags { 50 | ...SlimTagData 51 | __typename 52 | } 53 | ignore_auto_tag 54 | image_path 55 | scene_count 56 | # scene_count_all: scene_count(depth: -1) 57 | image_count 58 | # image_count_all: image_count(depth: -1) 59 | gallery_count 60 | # gallery_count_all: gallery_count(depth: -1) 61 | performer_count 62 | # performer_count_all: performer_count(depth: -1) 63 | group_count 64 | # group_count_all: group_count(depth: -1) 65 | details 66 | rating100 67 | aliases 68 | favorite 69 | __typename 70 | created_at 71 | updated_at 72 | } 73 | 74 | mutation UpdateStudio($input: StudioUpdateInput!){ 75 | studioUpdate(input: $input){ 76 | ...StudioData 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/graphql/FindTags.graphql: -------------------------------------------------------------------------------- 1 | query FindTags($filter: FindFilterType, $tag_filter: TagFilterType, $ids: [ID!]) { 2 | findTags(filter: $filter, tag_filter: $tag_filter, ids: $ids){ 3 | tags { 4 | ...TagData 5 | __typename 6 | } 7 | } 8 | } 9 | 10 | query GetTag($id: ID!) { 11 | findTag(id: $id) { 12 | ...TagData 13 | } 14 | } 15 | 16 | query CountTags($filter: FindFilterType, $tag_filter: TagFilterType) { 17 | findTags(filter: $filter, tag_filter: $tag_filter){ 18 | count 19 | } 20 | } 21 | 22 | mutation CreateTag($input: TagCreateInput!) { 23 | tagCreate(input: $input) { 24 | ...TagData 25 | } 26 | } 27 | 28 | mutation UpdateTag($input: TagUpdateInput!){ 29 | tagUpdate(input: $input){ 30 | ...TagData 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/graphql/Fragments.graphql: -------------------------------------------------------------------------------- 1 | fragment SlimTagData on Tag { 2 | id 3 | name 4 | } 5 | 6 | fragment TagData on Tag { 7 | id 8 | name 9 | description 10 | sort_name 11 | favorite 12 | aliases 13 | scene_count 14 | performer_count 15 | scene_marker_count 16 | image_path 17 | image_count 18 | gallery_count 19 | parent_count 20 | child_count 21 | created_at 22 | updated_at 23 | } 24 | 25 | fragment StashJob on Job { 26 | id 27 | status 28 | progress 29 | description 30 | subTasks 31 | addTime 32 | endTime 33 | error 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/graphql/ImageUpdate.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateImage($input: ImageUpdateInput!) { 2 | imageUpdate(input: $input) { 3 | id 4 | studio { 5 | ...StudioData 6 | } 7 | tags { 8 | ...TagData 9 | } 10 | performers { 11 | ...PerformerData 12 | } 13 | galleries { 14 | ...GalleryData 15 | } 16 | rating100 17 | } 18 | } 19 | 20 | mutation ImageIncrementO($id: ID!) { 21 | imageIncrementO(id: $id) 22 | } 23 | 24 | mutation ImageDecrementO($id: ID!) { 25 | imageDecrementO(id: $id) 26 | } 27 | 28 | mutation ImageResetO($id: ID!) { 29 | imageResetO(id: $id) 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/graphql/README.md: -------------------------------------------------------------------------------- 1 | # Stash GraphQL 2 | 3 | This directory contains the GraphQL query files. 4 | 5 | ## Queries 6 | 7 | Some of the query files (`*.graphql`) are modified from the queries in https://github.com/stashapp/stash/tree/develop/graphql. 8 | -------------------------------------------------------------------------------- /app/src/main/graphql/RunPluginTask.graphql: -------------------------------------------------------------------------------- 1 | mutation RunPluginTask($plugin_id: ID!, $task_name: String!, $args_map: Map) { 2 | runPluginTask(plugin_id: $plugin_id, task_name: $task_name, args_map: $args_map) 3 | } 4 | -------------------------------------------------------------------------------- /app/src/main/graphql/SaveSceneActivity.graphql: -------------------------------------------------------------------------------- 1 | mutation SceneSaveActivity($scene_id: ID!, $resume_time: Float!, $play_duration: Float) { 2 | sceneSaveActivity(id: $scene_id, resume_time: $resume_time, playDuration: $play_duration) 3 | } 4 | 5 | mutation SceneResetO($scene_id: ID!) { 6 | sceneResetO(id: $scene_id) 7 | } 8 | 9 | mutation SceneAddO($scene_id: ID!, $times: [Timestamp!]) { 10 | sceneAddO(id: $scene_id, times: $times) { 11 | count 12 | history 13 | } 14 | } 15 | 16 | mutation SceneDeleteO($scene_id: ID!, $times: [Timestamp!]) { 17 | sceneDeleteO(id: $scene_id, times: $times) { 18 | count 19 | history 20 | } 21 | } 22 | 23 | mutation SceneAddPlayCount($scene_id: ID!, $times: [Timestamp!]) { 24 | sceneAddPlay(id: $scene_id, times: $times) { 25 | count 26 | history 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/graphql/SceneUpdate.graphql: -------------------------------------------------------------------------------- 1 | mutation SceneUpdate($input: SceneUpdateInput!) { 2 | sceneUpdate(input: $input) { 3 | id 4 | tags { 5 | ...TagData 6 | } 7 | performers { 8 | ...PerformerData 9 | } 10 | studio { 11 | ...StudioData 12 | } 13 | groups { 14 | scene_index 15 | group{ 16 | ...GroupData 17 | } 18 | } 19 | galleries { 20 | ...GalleryData 21 | } 22 | rating100 23 | } 24 | } 25 | 26 | mutation CreateMarker($input: SceneMarkerCreateInput!) { 27 | sceneMarkerCreate(input: $input) { 28 | ...FullMarkerData 29 | } 30 | } 31 | 32 | mutation DeleteMarker($id: ID!) { 33 | sceneMarkerDestroy(id: $id) 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/graphql/ServerInfo.graphql: -------------------------------------------------------------------------------- 1 | query ServerInfo { 2 | version { 3 | version 4 | } 5 | findScenes { 6 | count 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/graphql/Subscriptions.graphql: -------------------------------------------------------------------------------- 1 | subscription JobProgress { 2 | jobsSubscribe { 3 | type 4 | job { 5 | ...StashJob 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/graphql/SystemStatus.graphql: -------------------------------------------------------------------------------- 1 | query SystemStatus { 2 | systemStatus { 3 | status 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/LicenseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.TextView 6 | import androidx.fragment.app.Fragment 7 | import com.github.damontecres.stashapp.util.concatIfNotBlank 8 | import java.io.BufferedReader 9 | 10 | /** 11 | * Show various licenses for third party libraries included in the app 12 | */ 13 | class LicenseFragment : Fragment(R.layout.license) { 14 | override fun onViewCreated( 15 | view: View, 16 | savedInstanceState: Bundle?, 17 | ) { 18 | super.onViewCreated(view, savedInstanceState) 19 | 20 | val attributions = resources.getTextArray(R.array.stash_license_attributions) 21 | val licenseText = 22 | resources.assets 23 | .open("LICENSE") 24 | .bufferedReader() 25 | .use(BufferedReader::readText) 26 | val otherLicenses = 27 | resources.assets 28 | .list("licenses")!! 29 | .map { 30 | resources.assets 31 | .open("licenses/$it") 32 | .bufferedReader() 33 | .use(BufferedReader::readText) 34 | }.toTypedArray() 35 | 36 | val text = 37 | concatIfNotBlank( 38 | getString(R.string.license_separator), 39 | *attributions, 40 | licenseText, 41 | *otherLicenses, 42 | ) 43 | 44 | view.findViewById<TextView>(R.id.license_text).text = text 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/UpdateChangelogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.TextView 6 | import androidx.fragment.app.Fragment 7 | import androidx.lifecycle.lifecycleScope 8 | import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler 9 | import com.github.damontecres.stashapp.util.UpdateChecker 10 | import io.noties.markwon.Markwon 11 | import kotlinx.coroutines.launch 12 | 13 | class UpdateChangelogFragment : Fragment(R.layout.changelog) { 14 | override fun onViewCreated( 15 | view: View, 16 | savedInstanceState: Bundle?, 17 | ) { 18 | super.onViewCreated(view, savedInstanceState) 19 | viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler(autoToast = true)) { 20 | val release = UpdateChecker.getLatestRelease(requireActivity()) 21 | if (release != null) { 22 | val changelogText = 23 | "# ${release.version}\n\n${release.body}" 24 | // Convert PR urls to number 25 | .replace( 26 | Regex("https://github.com/damontecres/StashAppAndroidTV/pull/(\\d+)"), 27 | "#$1", 28 | ) 29 | // Remove the last line for full changelog since its just a link 30 | .replace(Regex("\\*\\*Full Changelog\\*\\*.*"), "") 31 | val textView = view.findViewById<TextView>(R.id.changelog_text) 32 | val markdown = Markwon.create(requireContext()) 33 | markdown.setMarkdown(textView, changelogText) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/actions/CreateMarkerAction.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.actions 2 | 3 | data class CreateMarkerAction( 4 | val position: Long, 5 | ) { 6 | // This is kind of hacky 7 | val id get() = StashAction.CREATE_MARKER.id 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/actions/StashAction.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.actions 2 | 3 | /** 4 | * Represents an action a user can take 5 | * 6 | * This is sort of a catch-all, but [com.github.damontecres.stashapp.presenters.ActionPresenter] can render them 7 | */ 8 | enum class StashAction( 9 | val id: Long, 10 | val actionName: String, 11 | ) { 12 | ADD_TAG(1L, "Add Tag"), 13 | ADD_PERFORMER(2L, "Add Performer"), 14 | FORCE_TRANSCODE(3L, "Play with Transcoding"), 15 | CREATE_MARKER(4L, "Create Marker"), 16 | FORCE_DIRECT_PLAY(5L, "Play directly"), 17 | CREATE_NEW(6L, "Create new"), 18 | SET_STUDIO(7L, "Set Studio"), 19 | SHIFT_MARKERS(8L, "Change marker timestamp"), 20 | ADD_GALLERY(9L, "Add Gallery"), 21 | ADD_GROUP(10L, "Add Group"), 22 | ; 23 | 24 | companion object { 25 | // Actions that require searching for something 26 | val SEARCH_FOR_ACTIONS = 27 | setOf(ADD_TAG, ADD_PERFORMER, CREATE_MARKER, SET_STUDIO, ADD_GALLERY, ADD_GROUP) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/actions/StashActionClickedListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.actions 2 | 3 | import com.github.damontecres.stashapp.data.OCounter 4 | 5 | /** 6 | * Respond to clicks on a card for a [StashAction] 7 | */ 8 | interface StashActionClickedListener { 9 | fun onClicked(action: StashAction) 10 | 11 | fun incrementOCounter(counter: OCounter) 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/api/fragment/GroupRelationshipData.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.api.fragment 2 | 3 | enum class GroupRelationshipType { 4 | CONTAINING, 5 | SUB, 6 | } 7 | 8 | data class GroupRelationshipData( 9 | val group: GroupData, 10 | val type: GroupRelationshipType, 11 | val description: String?, 12 | ) : StashData { 13 | override val id: String = group.id 14 | } 15 | 16 | fun GroupDescriptionData.toRelationship(type: GroupRelationshipType): GroupRelationshipData = 17 | GroupRelationshipData( 18 | group.groupData, 19 | type, 20 | description, 21 | ) 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/api/fragment/StashData.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.api.fragment 2 | 3 | sealed interface StashData { 4 | val id: String 5 | 6 | override fun equals(other: Any?): Boolean 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/data/JobResult.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.data 2 | 3 | sealed class JobResult { 4 | data object NotFound : JobResult() 5 | 6 | data class Failure( 7 | val message: String?, 8 | ) : JobResult() 9 | 10 | data object Success : JobResult() 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/data/OCounter.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.data 2 | 3 | data class OCounter( 4 | val id: String, 5 | val count: Int, 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/data/PlaylistItem.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.data 2 | 3 | import com.github.damontecres.stashapp.api.fragment.MarkerData 4 | import com.github.damontecres.stashapp.api.fragment.SlimSceneData 5 | import com.github.damontecres.stashapp.util.joinNotNullOrBlank 6 | import com.github.damontecres.stashapp.util.titleOrFilename 7 | import kotlin.time.DurationUnit 8 | import kotlin.time.toDuration 9 | 10 | /** 11 | * Represents an item in a playlist/queue 12 | */ 13 | data class PlaylistItem( 14 | val index: Int, 15 | val title: CharSequence?, 16 | val subtitle: CharSequence?, 17 | val details1: CharSequence?, 18 | val details2: CharSequence?, 19 | val imageUrl: String?, 20 | ) 21 | 22 | fun MarkerData.toPlayListItem(index: Int): PlaylistItem { 23 | val name = 24 | title.ifBlank { 25 | primary_tag.slimTagData.name 26 | } 27 | val details = 28 | buildList { 29 | add(primary_tag.slimTagData.name) 30 | addAll(tags.map { it.slimTagData.name }) 31 | }.joinNotNullOrBlank(", ") 32 | return PlaylistItem( 33 | index, 34 | title = "$name - ${seconds.toInt().toDuration(DurationUnit.SECONDS)}", 35 | subtitle = scene.minimalSceneData.titleOrFilename, 36 | details1 = details, 37 | details2 = scene.minimalSceneData.date, 38 | imageUrl = screenshot, 39 | ) 40 | } 41 | 42 | fun SlimSceneData.toPlayListItem(index: Int): PlaylistItem = 43 | PlaylistItem( 44 | index, 45 | title = titleOrFilename, 46 | subtitle = studio?.name, 47 | details1 = performers.map { it.name }.joinNotNullOrBlank(", "), 48 | details2 = date, 49 | imageUrl = paths.screenshot, 50 | ) 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/data/room/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.data.room 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | 7 | @Database(entities = [RecentSearchItem::class, PlaybackEffect::class], version = 5) 8 | @TypeConverters(Converters::class) 9 | abstract class AppDatabase : RoomDatabase() { 10 | abstract fun recentSearchItemsDao(): RecentSearchItemsDao 11 | 12 | abstract fun playbackEffectsDao(): PlaybackEffectsDao 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/data/room/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.data.room 2 | 3 | import androidx.room.TypeConverter 4 | import com.github.damontecres.stashapp.data.DataType 5 | import java.util.Date 6 | 7 | class Converters { 8 | @TypeConverter 9 | fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) } 10 | 11 | @TypeConverter 12 | fun dateToTimestamp(date: Date?): Long? = date?.time 13 | 14 | @TypeConverter 15 | fun fromDataType(value: Int?): DataType? = value?.let { DataType.entries[it] } 16 | 17 | @TypeConverter 18 | fun dataTypeToInt(value: DataType?): Int? = value?.ordinal 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/data/room/Migrations.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.data.room 2 | 3 | import androidx.room.migration.Migration 4 | import androidx.sqlite.db.SupportSQLiteDatabase 5 | import com.github.damontecres.stashapp.data.DataType 6 | 7 | val MIGRATION_4_TO_5 = 8 | object : Migration(4, 5) { 9 | override fun migrate(db: SupportSQLiteDatabase) { 10 | // Create a new temporary table with the new primary key 11 | db.execSQL( 12 | "CREATE TABLE IF NOT EXISTS `playback_effects_temp` " + 13 | "(`serverUrl` TEXT NOT NULL, `id` TEXT NOT NULL, `dataType` INTEGER NOT NULL, " + 14 | "`rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, " + 15 | "`saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, " + 16 | "`red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, " + 17 | "`blur` INTEGER NOT NULL, PRIMARY KEY(`serverUrl`, `id`, `dataType`))", 18 | ) 19 | // Insert all of the data into the new temp table, defaulting the dataType to SCENE 20 | db.execSQL( 21 | "INSERT INTO playback_effects_temp " + 22 | "SELECT serverUrl, id, ${DataType.SCENE.ordinal}, rotation, brightness, contrast, " + 23 | "saturation, hue, red, green, blue, blur " + 24 | "FROM playback_effects", 25 | ) 26 | // Drop original table and rename temporary one 27 | db.execSQL("DROP TABLE playback_effects") 28 | db.execSQL("ALTER TABLE playback_effects_temp RENAME TO playback_effects") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/data/room/PlaybackEffect.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.data.room 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Entity 5 | import com.github.damontecres.stashapp.data.DataType 6 | import com.github.damontecres.stashapp.data.VideoFilter 7 | 8 | /** 9 | * Store a [VideoFilter] for a specific scene (by server & scene ID) 10 | */ 11 | @Entity(tableName = "playback_effects", primaryKeys = ["serverUrl", "id", "dataType"]) 12 | data class PlaybackEffect( 13 | val serverUrl: String, 14 | val id: String, 15 | val dataType: DataType, 16 | @Embedded val videoFilter: VideoFilter, 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/data/room/PlaybackEffectsDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.data.room 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.github.damontecres.stashapp.data.DataType 8 | 9 | /** 10 | * Store [PlaybackEffect]/][VideoFilter]s 11 | */ 12 | @Dao 13 | interface PlaybackEffectsDao { 14 | @Query("SELECT * FROM playback_effects WHERE serverUrl = :serverUrl AND id = :id AND dataType = :dataType") 15 | fun getPlaybackEffect( 16 | serverUrl: String, 17 | id: String, 18 | dataType: DataType, 19 | ): PlaybackEffect? 20 | 21 | @Query("SELECT * FROM playback_effects WHERE serverUrl = :serverUrl") 22 | fun getPlaybackEffects(serverUrl: String): List<PlaybackEffect> 23 | 24 | @Insert(onConflict = OnConflictStrategy.REPLACE) 25 | fun insert(vararg items: PlaybackEffect) 26 | 27 | @Query("DELETE FROM playback_effects") 28 | fun deleteAll() 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/data/room/RecentSearchItem.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.data.room 2 | 3 | import androidx.room.Entity 4 | import com.github.damontecres.stashapp.data.DataType 5 | import java.util.Date 6 | 7 | /** 8 | * Represents an item that the user recently used in a search-for task 9 | * 10 | * Must be identified by server, data type, & ID 11 | */ 12 | @Entity(tableName = "recent_search_items", primaryKeys = ["serverUrl", "id", "dataType"]) 13 | data class RecentSearchItem( 14 | val serverUrl: String, 15 | val id: String, 16 | val dataType: DataType, 17 | val timestamp: Date = Date(), 18 | ) 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/data/room/RecentSearchItemsDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.data.room 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import com.github.damontecres.stashapp.data.DataType 9 | 10 | /** 11 | * Retrieve and store [RecentSearchItem]s 12 | */ 13 | @Dao 14 | interface RecentSearchItemsDao { 15 | @Query("SELECT * FROM recent_search_items WHERE serverUrl = :serverUrl AND dataType = :dataType ORDER BY timestamp DESC LIMIT :count") 16 | fun getMostRecent( 17 | count: Int = 25, 18 | serverUrl: String, 19 | dataType: DataType, 20 | ): List<RecentSearchItem> 21 | 22 | @Insert(onConflict = OnConflictStrategy.REPLACE) 23 | fun insert(vararg items: RecentSearchItem) 24 | 25 | @Delete 26 | fun delete(vararg items: RecentSearchItem) 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.filter 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.fragment.app.activityViewModels 7 | import androidx.leanback.app.GuidedStepSupportFragment 8 | import com.github.damontecres.stashapp.R 9 | import com.github.damontecres.stashapp.navigation.Destination 10 | import com.github.damontecres.stashapp.util.getDestination 11 | 12 | class CreateFilterFragment : Fragment(R.layout.frame) { 13 | private val viewModel by activityViewModels<CreateFilterViewModel>() 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | 18 | val dest = requireArguments().getDestination<Destination.CreateFilter>() 19 | val startingFilter = dest.startingFilter 20 | viewModel.initialize( 21 | dest.dataType, 22 | startingFilter, 23 | ) 24 | } 25 | 26 | override fun onViewCreated( 27 | view: View, 28 | savedInstanceState: Bundle?, 29 | ) { 30 | super.onViewCreated(view, savedInstanceState) 31 | viewModel.ready.observe(viewLifecycleOwner) { ready -> 32 | if (ready) { 33 | GuidedStepSupportFragment.add( 34 | childFragmentManager, 35 | CreateFilterStep(), 36 | R.id.frame_content, 37 | ) 38 | } 39 | } 40 | } 41 | 42 | companion object { 43 | private const val TAG = "CreateFilterFragment" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/filter/picker/FloatPickerFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.filter.picker 2 | 3 | import android.os.Bundle 4 | import android.text.InputType 5 | import androidx.leanback.widget.GuidedAction 6 | import com.apollographql.apollo.api.Optional 7 | import com.github.damontecres.stashapp.api.type.CriterionModifier 8 | import com.github.damontecres.stashapp.api.type.FloatCriterionInput 9 | import com.github.damontecres.stashapp.api.type.StashDataFilter 10 | import com.github.damontecres.stashapp.filter.FilterOption 11 | 12 | /** 13 | * Pick a decimal/float value 14 | */ 15 | class FloatPickerFragment( 16 | filterOption: FilterOption<StashDataFilter, FloatCriterionInput>, 17 | ) : TwoValuePicker<Double, FloatCriterionInput>(filterOption) { 18 | override val valueInputType: Int 19 | get() = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL 20 | 21 | override fun parseValue(v: String?): Double? = v?.toDoubleOrNull() 22 | 23 | override fun createCriterionInput( 24 | value1: Double?, 25 | value2: Double?, 26 | modifier: CriterionModifier, 27 | ): FloatCriterionInput? = 28 | if (value1 != null) { 29 | FloatCriterionInput( 30 | value = value1, 31 | value2 = Optional.presentIfNotNull(value2), 32 | modifier = modifier, 33 | ) 34 | } else if (modifier == CriterionModifier.IS_NULL || modifier == CriterionModifier.NOT_NULL) { 35 | FloatCriterionInput(value = 0.0, modifier = modifier) 36 | } else { 37 | null 38 | } 39 | 40 | override fun onCreateActions( 41 | actions: MutableList<GuidedAction>, 42 | savedInstanceState: Bundle?, 43 | ) { 44 | val curVal = filterOption.getter(viewModel.objectFilter.value!!).getOrNull() 45 | value1 = curVal?.value 46 | value2 = curVal?.value2?.getOrNull() 47 | modifier = curVal?.modifier ?: CriterionModifier.EQUALS 48 | createActionList(actions) 49 | } 50 | 51 | companion object { 52 | private const val TAG = "FloatPickerFragment" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/filter/picker/GuidedDurationPickerAction.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.filter.picker 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import androidx.leanback.widget.GuidedAction 6 | 7 | /** 8 | * [GuidedAction] for picking a duration 9 | */ 10 | class GuidedDurationPickerAction : GuidedAction() { 11 | class Builder( 12 | context: Context, 13 | ) : BuilderBase<Builder>(context) { 14 | private var duration = 0 15 | 16 | init { 17 | hasEditableActivatorView(true) 18 | } 19 | 20 | fun duration(duration: Int): Builder { 21 | this.duration = duration 22 | return this 23 | } 24 | 25 | fun build(): GuidedDurationPickerAction { 26 | val action = GuidedDurationPickerAction() 27 | action.duration = duration 28 | super.applyValues(action) 29 | return action 30 | } 31 | } 32 | 33 | var duration: Int = 0 34 | 35 | override fun onSaveInstanceState( 36 | bundle: Bundle, 37 | key: String, 38 | ) { 39 | bundle.putInt(key, duration) 40 | } 41 | 42 | override fun onRestoreInstanceState( 43 | bundle: Bundle, 44 | key: String, 45 | ) { 46 | duration = bundle.getInt(key) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/filter/picker/IntPickerFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.filter.picker 2 | 3 | import android.os.Bundle 4 | import android.text.InputType 5 | import androidx.leanback.widget.GuidedAction 6 | import com.apollographql.apollo.api.Optional 7 | import com.github.damontecres.stashapp.api.type.CriterionModifier 8 | import com.github.damontecres.stashapp.api.type.IntCriterionInput 9 | import com.github.damontecres.stashapp.api.type.StashDataFilter 10 | import com.github.damontecres.stashapp.filter.FilterOption 11 | 12 | class IntPickerFragment( 13 | filterOption: FilterOption<StashDataFilter, IntCriterionInput>, 14 | ) : TwoValuePicker<Int, IntCriterionInput>(filterOption) { 15 | override val valueInputType: Int 16 | get() = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_SIGNED 17 | 18 | override fun parseValue(v: String?): Int? = v?.toIntOrNull() 19 | 20 | override fun createCriterionInput( 21 | value1: Int?, 22 | value2: Int?, 23 | modifier: CriterionModifier, 24 | ): IntCriterionInput? = 25 | if (value1 != null) { 26 | IntCriterionInput( 27 | value = value1, 28 | value2 = Optional.presentIfNotNull(value2), 29 | modifier = modifier, 30 | ) 31 | } else if (modifier == CriterionModifier.IS_NULL || modifier == CriterionModifier.NOT_NULL) { 32 | IntCriterionInput(value = 0, modifier = modifier) 33 | } else { 34 | null 35 | } 36 | 37 | override fun onCreateActions( 38 | actions: MutableList<GuidedAction>, 39 | savedInstanceState: Bundle?, 40 | ) { 41 | val curVal = filterOption.getter(viewModel.objectFilter.value!!).getOrNull() 42 | value1 = curVal?.value 43 | value2 = curVal?.value2?.getOrNull() 44 | modifier = curVal?.modifier ?: CriterionModifier.EQUALS 45 | createActionList(actions) 46 | } 47 | 48 | companion object { 49 | private const val TAG = "IntPickerFragment" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/image/ImageController.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.image 2 | 3 | /** 4 | * Interface to expose making modifications to an image 5 | */ 6 | interface ImageController { 7 | fun zoomIn() 8 | 9 | fun zoomOut() 10 | 11 | fun rotateLeft() 12 | 13 | fun rotateRight() 14 | 15 | fun flip() 16 | 17 | fun reset(animate: Boolean = true) 18 | 19 | fun isImageZoomedIn(): Boolean 20 | } 21 | 22 | /** 23 | * Interface to expose controls for a video 24 | */ 25 | interface VideoController { 26 | fun play() 27 | 28 | fun pause() 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/navigation/FilterAndPosition.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.navigation 2 | 3 | import com.github.damontecres.stashapp.suppliers.FilterArgs 4 | 5 | data class FilterAndPosition( 6 | val filter: FilterArgs, 7 | val position: Int, 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/playback/ControllerVisibilityListenerList.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.playback 2 | 3 | import androidx.media3.ui.PlayerView 4 | 5 | /** 6 | * Since a [PlayerView] can only have one [PlayerView.ControllerVisibilityListener], this class allows for multiple by maintaining a list of them 7 | */ 8 | class ControllerVisibilityListenerList : PlayerView.ControllerVisibilityListener { 9 | private val listeners = mutableListOf<PlayerView.ControllerVisibilityListener>() 10 | 11 | fun addListener(listener: PlayerView.ControllerVisibilityListener) { 12 | listeners.add(listener) 13 | } 14 | 15 | override fun onVisibilityChanged(visibility: Int) { 16 | listeners.forEach { it.onVisibilityChanged(visibility) } 17 | } 18 | 19 | fun removeAllListeners() { 20 | listeners.clear() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/playback/PlaylistScenesFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.playback 2 | 3 | import androidx.annotation.OptIn 4 | import androidx.media3.common.MediaItem 5 | import androidx.media3.common.util.UnstableApi 6 | import com.github.damontecres.stashapp.api.CountScenesQuery 7 | import com.github.damontecres.stashapp.api.FindVideoScenesQuery 8 | import com.github.damontecres.stashapp.api.fragment.SlimSceneData 9 | import com.github.damontecres.stashapp.api.fragment.VideoSceneData 10 | import com.github.damontecres.stashapp.data.DataType 11 | import com.github.damontecres.stashapp.data.Scene 12 | 13 | /** 14 | * A [PlaylistFragment] that plays [SlimSceneData] videos 15 | */ 16 | @OptIn(UnstableApi::class) 17 | class PlaylistScenesFragment : PlaylistFragment<FindVideoScenesQuery.Data, VideoSceneData, CountScenesQuery.Data>() { 18 | override val previewsEnabled: Boolean 19 | get() = true 20 | 21 | override val optionsButtonOptions: OptionsButtonOptions 22 | get() = OptionsButtonOptions(DataType.SCENE, true) 23 | 24 | override val activityTrackingEnabled: Boolean 25 | get() = true 26 | 27 | override fun builderCallback(item: VideoSceneData): (MediaItem.Builder.() -> Unit)? = null 28 | 29 | override fun convertToScene(item: VideoSceneData): Scene = Scene.fromVideoSceneData(item) 30 | 31 | companion object { 32 | private const val TAG = "PlaylistScenesFragment" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/presenters/ActionPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.presenters 2 | 3 | import com.github.damontecres.stashapp.actions.StashAction 4 | 5 | class ActionPresenter( 6 | callback: LongClickCallBack<StashAction>? = null, 7 | ) : StashPresenter<StashAction>(callback) { 8 | override fun doOnBindViewHolder( 9 | cardView: StashImageCardView, 10 | item: StashAction, 11 | ) { 12 | cardView.titleText = item.actionName 13 | cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT) 14 | } 15 | 16 | companion object { 17 | private const val TAG = "ActionPresenter" 18 | 19 | const val CARD_WIDTH = 235 20 | const val CARD_HEIGHT = 160 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/presenters/CreateMarkerActionPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.presenters 2 | 3 | import com.github.damontecres.stashapp.actions.CreateMarkerAction 4 | import com.github.damontecres.stashapp.views.durationToString 5 | 6 | class CreateMarkerActionPresenter( 7 | callback: LongClickCallBack<CreateMarkerAction>? = null, 8 | ) : StashPresenter<CreateMarkerAction>(callback) { 9 | override fun doOnBindViewHolder( 10 | cardView: StashImageCardView, 11 | item: CreateMarkerAction, 12 | ) { 13 | cardView.titleText = "Create Marker" 14 | cardView.contentText = durationToString(item.position / 1000.0) 15 | cardView.setMainImageDimensions( 16 | ActionPresenter.CARD_WIDTH, 17 | ActionPresenter.CARD_HEIGHT, 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/presenters/GalleryPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.presenters 2 | 3 | import com.github.damontecres.stashapp.R 4 | import com.github.damontecres.stashapp.api.fragment.GalleryData 5 | import com.github.damontecres.stashapp.data.DataType 6 | import com.github.damontecres.stashapp.util.concatIfNotBlank 7 | import com.github.damontecres.stashapp.util.name 8 | import java.util.EnumMap 9 | 10 | class GalleryPresenter( 11 | callback: LongClickCallBack<GalleryData>? = null, 12 | ) : StashPresenter<GalleryData>(callback) { 13 | override fun doOnBindViewHolder( 14 | cardView: StashImageCardView, 15 | item: GalleryData, 16 | ) { 17 | cardView.blackImageBackground = false 18 | cardView.titleText = item.name 19 | 20 | val details = mutableListOf<String?>() 21 | details.add(item.studio?.name) 22 | details.add(item.date) 23 | cardView.contentText = concatIfNotBlank(" - ", details) 24 | 25 | val dataTypeMap = EnumMap<DataType, Int>(DataType::class.java) 26 | dataTypeMap[DataType.TAG] = item.tags.size 27 | dataTypeMap[DataType.PERFORMER] = item.performers.size 28 | dataTypeMap[DataType.SCENE] = item.scenes.size 29 | dataTypeMap[DataType.IMAGE] = item.image_count 30 | 31 | cardView.setUpExtraRow(dataTypeMap, null) 32 | 33 | cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT) 34 | val coverImage = item.paths.cover 35 | loadImage(cardView, coverImage, defaultDrawable = R.drawable.default_gallery) 36 | 37 | cardView.setRating100(item.rating100) 38 | } 39 | 40 | override fun imageMatchParent(item: GalleryData): Boolean = item.paths.cover.isBlank() || item.paths.cover.isDefaultUrl 41 | 42 | companion object { 43 | private const val TAG = "GalleryPresenter" 44 | 45 | const val CARD_WIDTH = ImagePresenter.CARD_WIDTH 46 | const val CARD_HEIGHT = ImagePresenter.CARD_HEIGHT 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/presenters/GroupRelationshipPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.presenters 2 | 3 | import com.github.damontecres.stashapp.api.fragment.GroupRelationshipData 4 | 5 | class GroupRelationshipPresenter( 6 | callback: LongClickCallBack<GroupRelationshipData>? = null, 7 | ) : StashPresenter<GroupRelationshipData>( 8 | callback, 9 | ) { 10 | private val groupPresenter = GroupPresenter() 11 | 12 | override fun doOnBindViewHolder( 13 | cardView: StashImageCardView, 14 | item: GroupRelationshipData, 15 | ) { 16 | groupPresenter.doOnBindViewHolder(cardView, item.group) 17 | cardView.contentText = item.description 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/presenters/NullPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.presenters 2 | 3 | import com.github.damontecres.stashapp.data.DataType 4 | import com.github.damontecres.stashapp.util.defaultCardHeight 5 | import com.github.damontecres.stashapp.util.defaultCardWidth 6 | 7 | /** 8 | * A no-op implementation of [StashPresenter] that accepts a null item 9 | */ 10 | class NullPresenter( 11 | private val width: Int, 12 | private val height: Int, 13 | ) : StashPresenter<Any?>() { 14 | constructor(dataType: DataType) : this(dataType.defaultCardWidth, dataType.defaultCardHeight) 15 | 16 | override fun doOnBindViewHolder( 17 | cardView: StashImageCardView, 18 | item: Any?, 19 | ) { 20 | // no-op 21 | } 22 | 23 | fun bindNull(cardView: StashImageCardView) { 24 | cardView.setMainImageDimensions(width, height) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/presenters/NullPresenterSelector.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.presenters 2 | 3 | import androidx.leanback.widget.Presenter 4 | import androidx.leanback.widget.PresenterSelector 5 | 6 | /** 7 | * A [PresenterSelector] that delegates to another [PresenterSelector] except when the item is null, it return the specified [Presenter] 8 | */ 9 | class NullPresenterSelector( 10 | private val presenterSelector: PresenterSelector, 11 | private val nullPresenter: Presenter, 12 | ) : PresenterSelector() { 13 | override fun getPresenter(item: Any?): Presenter = 14 | if (item == null) { 15 | nullPresenter 16 | } else { 17 | presenterSelector.getPresenter(item) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/presenters/OCounterPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.presenters 2 | 3 | import com.github.damontecres.stashapp.R 4 | import com.github.damontecres.stashapp.data.OCounter 5 | 6 | class OCounterPresenter( 7 | callback: LongClickCallBack<OCounter>? = null, 8 | ) : StashPresenter<OCounter>(callback) { 9 | override fun doOnBindViewHolder( 10 | cardView: StashImageCardView, 11 | item: OCounter, 12 | ) { 13 | cardView.blackImageBackground = false 14 | 15 | val text = cardView.context.getString(R.string.stashapp_o_counter) 16 | cardView.titleText = "$text (${item.count})" 17 | cardView.setMainImageDimensions(ActionPresenter.CARD_WIDTH, ActionPresenter.CARD_HEIGHT) 18 | cardView.mainImageView.setPadding( 19 | IMAGE_PADDING, 20 | IMAGE_PADDING, 21 | IMAGE_PADDING, 22 | IMAGE_PADDING, 23 | ) 24 | loadImage(cardView, R.drawable.sweat_drops) 25 | } 26 | 27 | companion object { 28 | const val IMAGE_PADDING = (ActionPresenter.CARD_WIDTH * .07).toInt() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/presenters/PerformerInScenePresenter.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.presenters 2 | 3 | import android.os.Build 4 | import com.github.damontecres.stashapp.R 5 | import com.github.damontecres.stashapp.api.fragment.PerformerData 6 | import java.time.LocalDate 7 | import java.time.Period 8 | import java.time.format.DateTimeFormatter 9 | 10 | /** 11 | * A [PerformerPresenter] which will use the age of a [PerformerData] at specified date for the content text 12 | */ 13 | class PerformerInScenePresenter( 14 | private val date: String?, 15 | callback: LongClickCallBack<PerformerData>? = null, 16 | ) : PerformerPresenter( 17 | callback, 18 | ) { 19 | override fun getContentText( 20 | cardView: StashImageCardView, 21 | item: PerformerData, 22 | ): CharSequence? = 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 24 | val ageInScene = 25 | if (item.birthdate != null && date != null) { 26 | Period 27 | .between( 28 | LocalDate.parse(item.birthdate, DateTimeFormatter.ISO_LOCAL_DATE), 29 | LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE), 30 | ).years 31 | } else { 32 | null 33 | } 34 | if (ageInScene != null) { 35 | val yearsOld = cardView.context.getString(R.string.stashapp_years_old) 36 | val yearsOldStr = 37 | cardView.context.getString( 38 | R.string.stashapp_media_info_performer_card_age_context, 39 | ageInScene.toString(), 40 | yearsOld, 41 | ) 42 | yearsOldStr 43 | } else { 44 | super.getContentText(cardView, item) 45 | } 46 | } else { 47 | super.getContentText(cardView, item) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/presenters/PopupOnLongClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.presenters 2 | 3 | import android.view.View 4 | import android.view.View.OnLongClickListener 5 | import android.widget.AdapterView 6 | import android.widget.ArrayAdapter 7 | import androidx.appcompat.widget.ListPopupWindow 8 | import com.github.damontecres.stashapp.R 9 | import com.github.damontecres.stashapp.util.getMaxMeasuredWidth 10 | 11 | /** 12 | * An OnLongClickListener which shows a popup of predefined options 13 | */ 14 | class PopupOnLongClickListener( 15 | private val popupOptions: List<String>, 16 | private val popUpWidth: Int = ListPopupWindow.WRAP_CONTENT, 17 | private val popupItemClickListener: AdapterView.OnItemClickListener, 18 | ) : OnLongClickListener { 19 | override fun onLongClick(view: View): Boolean { 20 | if (popupOptions.isEmpty()) { 21 | return true 22 | } 23 | val listPopUp = 24 | ListPopupWindow( 25 | view.context, 26 | null, 27 | android.R.attr.listPopupWindowStyle, 28 | ) 29 | val adapter = 30 | ArrayAdapter( 31 | view.context, 32 | R.layout.popup_item, 33 | popupOptions, 34 | ) 35 | 36 | listPopUp.inputMethodMode = ListPopupWindow.INPUT_METHOD_NEEDED 37 | listPopUp.anchorView = view 38 | listPopUp.width = 39 | if (popUpWidth == ListPopupWindow.WRAP_CONTENT) { 40 | getMaxMeasuredWidth(view.context, adapter) 41 | } else { 42 | popUpWidth 43 | } 44 | listPopUp.isModal = true 45 | 46 | listPopUp.setAdapter(adapter) 47 | 48 | listPopUp.setOnItemClickListener { parent: AdapterView<*>, v: View, position: Int, id: Long -> 49 | popupItemClickListener.onItemClick(parent, v, position, id) 50 | listPopUp.dismiss() 51 | } 52 | 53 | listPopUp.show() 54 | 55 | return true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/setup/SetupFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.setup 2 | 3 | import android.os.Bundle 4 | import androidx.core.content.ContextCompat 5 | import androidx.leanback.widget.GuidanceStylist 6 | import androidx.leanback.widget.GuidedAction 7 | import com.github.damontecres.stashapp.R 8 | 9 | class SetupFragment : SetupGuidedStepSupportFragment() { 10 | override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance = 11 | GuidanceStylist.Guidance( 12 | getString(R.string.app_name_long), 13 | getString(R.string.first_time_setup), 14 | null, 15 | ContextCompat.getDrawable(requireContext(), R.mipmap.stash_logo), 16 | ) 17 | 18 | override fun onCreateActions( 19 | actions: MutableList<GuidedAction>, 20 | savedInstanceState: Bundle?, 21 | ) { 22 | actions.add( 23 | GuidedAction 24 | .Builder(requireContext()) 25 | .id(GuidedAction.ACTION_ID_CONTINUE) 26 | .hasNext(true) 27 | .title(R.string.stashapp_actions_continue) 28 | .build(), 29 | ) 30 | } 31 | 32 | override fun onGuidedActionClicked(action: GuidedAction) { 33 | nextStep(SetupStep1ServerUrl()) 34 | } 35 | 36 | companion object { 37 | const val ACTION_SERVER_URL = 98L 38 | const val ACTION_SERVER_API_KEY = 99L 39 | const val ACTION_PASSWORD_VISIBLE = 100L 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/setup/SetupState.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.setup 2 | 3 | import com.github.damontecres.stashapp.util.StashServer 4 | 5 | data class SetupState( 6 | val serverUrl: String, 7 | val apiKey: String?, 8 | val trustCerts: Boolean = false, 9 | val pinCode: String? = null, 10 | ) { 11 | constructor(serverUrl: CharSequence) : this(serverUrl.toString(), null) 12 | 13 | val stashServer get() = StashServer(serverUrl, apiKey) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/suppliers/ImageDataSupplier.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.suppliers 2 | 3 | import com.apollographql.apollo.api.Query 4 | import com.github.damontecres.stashapp.api.CountImagesQuery 5 | import com.github.damontecres.stashapp.api.FindImagesQuery 6 | import com.github.damontecres.stashapp.api.fragment.ImageData 7 | import com.github.damontecres.stashapp.api.type.FindFilterType 8 | import com.github.damontecres.stashapp.api.type.ImageFilterType 9 | import com.github.damontecres.stashapp.data.DataType 10 | import com.github.damontecres.stashapp.data.merge 11 | 12 | class ImageDataSupplier( 13 | private val findFilter: FindFilterType?, 14 | private val imageFilter: ImageFilterType?, 15 | ) : StashPagingSource.DataSupplier<FindImagesQuery.Data, ImageData, CountImagesQuery.Data> { 16 | constructor(imageFilter: ImageFilterType? = null) : this( 17 | DataType.IMAGE.asDefaultFindFilterType, 18 | imageFilter, 19 | ) 20 | 21 | override val dataType: DataType get() = DataType.TAG 22 | 23 | override fun createQuery(filter: FindFilterType?): Query<FindImagesQuery.Data> = 24 | FindImagesQuery( 25 | filter = filter, 26 | image_filter = imageFilter, 27 | ids = null, 28 | ) 29 | 30 | override fun getDefaultFilter(): FindFilterType = DataType.IMAGE.asDefaultFindFilterType.merge(findFilter) 31 | 32 | override fun createCountQuery(filter: FindFilterType?): Query<CountImagesQuery.Data> = CountImagesQuery(filter, imageFilter) 33 | 34 | override fun parseCountQuery(data: CountImagesQuery.Data): Int = data.findImages.count 35 | 36 | override fun parseQuery(data: FindImagesQuery.Data): List<ImageData> = data.findImages.images.map { it.imageData } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/suppliers/PerformerDataSupplier.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.suppliers 2 | 3 | import com.apollographql.apollo.api.Query 4 | import com.github.damontecres.stashapp.api.CountPerformersQuery 5 | import com.github.damontecres.stashapp.api.FindPerformersQuery 6 | import com.github.damontecres.stashapp.api.fragment.PerformerData 7 | import com.github.damontecres.stashapp.api.type.FindFilterType 8 | import com.github.damontecres.stashapp.api.type.PerformerFilterType 9 | import com.github.damontecres.stashapp.data.DataType 10 | import com.github.damontecres.stashapp.data.merge 11 | 12 | class PerformerDataSupplier( 13 | private val findFilter: FindFilterType?, 14 | private val performerFilter: PerformerFilterType?, 15 | ) : StashPagingSource.DataSupplier<FindPerformersQuery.Data, PerformerData, CountPerformersQuery.Data> { 16 | override val dataType: DataType get() = DataType.PERFORMER 17 | 18 | override fun createQuery(filter: FindFilterType?): Query<FindPerformersQuery.Data> = 19 | FindPerformersQuery( 20 | filter = filter, 21 | performer_filter = performerFilter, 22 | ids = null, 23 | ) 24 | 25 | override fun getDefaultFilter(): FindFilterType = DataType.PERFORMER.asDefaultFindFilterType.merge(findFilter) 26 | 27 | override fun createCountQuery(filter: FindFilterType?): Query<CountPerformersQuery.Data> = 28 | CountPerformersQuery(filter, performerFilter, null) 29 | 30 | override fun parseCountQuery(data: CountPerformersQuery.Data): Int = data.findPerformers.count 31 | 32 | override fun parseQuery(data: FindPerformersQuery.Data): List<PerformerData> = data.findPerformers.performers.map { it.performerData } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/suppliers/PerformerTagDataSupplier.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.suppliers 2 | 3 | import com.apollographql.apollo.api.Query 4 | import com.github.damontecres.stashapp.api.FindPerformerTagsQuery 5 | import com.github.damontecres.stashapp.api.fragment.TagData 6 | import com.github.damontecres.stashapp.api.type.FindFilterType 7 | import com.github.damontecres.stashapp.data.DataType 8 | 9 | /** 10 | * A DataSupplier that returns the tags for a performer 11 | */ 12 | class PerformerTagDataSupplier( 13 | private val performerId: String, 14 | ) : StashPagingSource.DataSupplier<FindPerformerTagsQuery.Data, TagData, FindPerformerTagsQuery.Data> { 15 | override val dataType: DataType 16 | get() = DataType.TAG 17 | 18 | override fun createQuery(filter: FindFilterType?): Query<FindPerformerTagsQuery.Data> = 19 | FindPerformerTagsQuery( 20 | filter = filter, 21 | performer_filter = null, 22 | ids = listOf(performerId), 23 | ) 24 | 25 | override fun getDefaultFilter(): FindFilterType = DataType.TAG.asDefaultFindFilterType 26 | 27 | override fun createCountQuery(filter: FindFilterType?): Query<FindPerformerTagsQuery.Data> = createQuery(filter) 28 | 29 | override fun parseCountQuery(data: FindPerformerTagsQuery.Data): Int = 30 | data.findPerformers.performers 31 | .firstOrNull() 32 | ?.tags 33 | ?.size ?: 0 34 | 35 | override fun parseQuery(data: FindPerformerTagsQuery.Data): List<TagData> = 36 | data.findPerformers.performers 37 | .firstOrNull() 38 | ?.tags 39 | ?.map { 40 | it.tagData 41 | }.orEmpty() 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/suppliers/SceneDataSupplier.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.suppliers 2 | 3 | import com.apollographql.apollo.api.Query 4 | import com.github.damontecres.stashapp.api.CountScenesQuery 5 | import com.github.damontecres.stashapp.api.FindScenesQuery 6 | import com.github.damontecres.stashapp.api.fragment.SlimSceneData 7 | import com.github.damontecres.stashapp.api.type.FindFilterType 8 | import com.github.damontecres.stashapp.api.type.SceneFilterType 9 | import com.github.damontecres.stashapp.data.DataType 10 | import com.github.damontecres.stashapp.data.merge 11 | 12 | class SceneDataSupplier( 13 | private val findFilter: FindFilterType?, 14 | private val sceneFilter: SceneFilterType? = null, 15 | ) : StashPagingSource.DataSupplier<FindScenesQuery.Data, SlimSceneData, CountScenesQuery.Data> { 16 | override val dataType: DataType get() = DataType.SCENE 17 | 18 | override fun createQuery(filter: FindFilterType?): Query<FindScenesQuery.Data> = 19 | FindScenesQuery( 20 | filter = filter, 21 | scene_filter = sceneFilter, 22 | ids = null, 23 | ) 24 | 25 | override fun parseQuery(data: FindScenesQuery.Data): List<SlimSceneData> = data.findScenes.scenes.map { it.slimSceneData } 26 | 27 | override fun getDefaultFilter(): FindFilterType = DataType.SCENE.asDefaultFindFilterType.merge(findFilter) 28 | 29 | override fun createCountQuery(filter: FindFilterType?): Query<CountScenesQuery.Data> = CountScenesQuery(filter, sceneFilter, null) 30 | 31 | override fun parseCountQuery(data: CountScenesQuery.Data): Int = data.findScenes.count 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/suppliers/TagDataSupplier.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.suppliers 2 | 3 | import com.apollographql.apollo.api.Query 4 | import com.github.damontecres.stashapp.api.CountTagsQuery 5 | import com.github.damontecres.stashapp.api.FindTagsQuery 6 | import com.github.damontecres.stashapp.api.fragment.TagData 7 | import com.github.damontecres.stashapp.api.type.FindFilterType 8 | import com.github.damontecres.stashapp.api.type.TagFilterType 9 | import com.github.damontecres.stashapp.data.DataType 10 | import com.github.damontecres.stashapp.data.merge 11 | 12 | class TagDataSupplier( 13 | private val findFilter: FindFilterType?, 14 | private val tagFilter: TagFilterType?, 15 | ) : StashPagingSource.DataSupplier<FindTagsQuery.Data, TagData, CountTagsQuery.Data> { 16 | override val dataType: DataType get() = DataType.TAG 17 | 18 | override fun createQuery(filter: FindFilterType?): Query<FindTagsQuery.Data> = 19 | FindTagsQuery( 20 | filter = filter, 21 | tag_filter = tagFilter, 22 | ids = null, 23 | ) 24 | 25 | override fun getDefaultFilter(): FindFilterType = DataType.TAG.asDefaultFindFilterType.merge(findFilter) 26 | 27 | override fun createCountQuery(filter: FindFilterType?): Query<CountTagsQuery.Data> = CountTagsQuery(filter, tagFilter) 28 | 29 | override fun parseCountQuery(data: CountTagsQuery.Data): Int = data.findTags.count 30 | 31 | override fun parseQuery(data: FindTagsQuery.Data): List<TagData> = data.findTags.tags.map { it.tagData } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/suppliers/VideoSceneDataSupplier.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.suppliers 2 | 3 | import com.apollographql.apollo.api.Query 4 | import com.github.damontecres.stashapp.api.CountScenesQuery 5 | import com.github.damontecres.stashapp.api.FindVideoScenesQuery 6 | import com.github.damontecres.stashapp.api.fragment.VideoSceneData 7 | import com.github.damontecres.stashapp.api.type.FindFilterType 8 | import com.github.damontecres.stashapp.api.type.SceneFilterType 9 | import com.github.damontecres.stashapp.data.DataType 10 | 11 | class VideoSceneDataSupplier( 12 | private val findFilter: FindFilterType?, 13 | private val sceneFilter: SceneFilterType? = null, 14 | ) : StashPagingSource.DataSupplier<FindVideoScenesQuery.Data, VideoSceneData, CountScenesQuery.Data> { 15 | override val dataType: DataType get() = DataType.SCENE 16 | 17 | override fun createQuery(filter: FindFilterType?): Query<FindVideoScenesQuery.Data> = 18 | FindVideoScenesQuery( 19 | filter = filter, 20 | scene_filter = sceneFilter, 21 | ids = null, 22 | ) 23 | 24 | override fun parseQuery(data: FindVideoScenesQuery.Data): List<VideoSceneData> = data.findScenes.scenes.map { it.videoSceneData } 25 | 26 | override fun getDefaultFilter(): FindFilterType = findFilter ?: DataType.SCENE.asDefaultFindFilterType 27 | 28 | override fun createCountQuery(filter: FindFilterType?): Query<CountScenesQuery.Data> = CountScenesQuery(filter, sceneFilter, null) 29 | 30 | override fun parseCountQuery(data: CountScenesQuery.Data): Int = data.findScenes.count 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/ui/components/CircularProgress.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.CircularProgressIndicator 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.tv.material3.MaterialTheme 10 | import com.github.damontecres.stashapp.ui.Material3AppTheme 11 | import com.github.damontecres.stashapp.ui.util.ifElse 12 | 13 | @Composable 14 | fun CircularProgress( 15 | modifier: Modifier = Modifier, 16 | fillMaxSize: Boolean = true, 17 | ) { 18 | Material3AppTheme { 19 | Box(modifier = modifier.ifElse(fillMaxSize, Modifier.fillMaxSize())) { 20 | CircularProgressIndicator( 21 | color = MaterialTheme.colorScheme.border, 22 | modifier = Modifier.align(Alignment.Center), 23 | ) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/ui/components/ItemOnClicker.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.ui.components 2 | 3 | import com.github.damontecres.stashapp.navigation.FilterAndPosition 4 | 5 | fun interface ItemOnClicker<T> { 6 | fun onClick( 7 | item: T, 8 | filterAndPosition: FilterAndPosition?, 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/AmbientPlayerListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.ui.components.playback 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.ui.platform.LocalContext 6 | import androidx.media3.common.Player 7 | import androidx.media3.common.Player.Listener 8 | import com.github.damontecres.stashapp.util.findActivity 9 | import com.github.damontecres.stashapp.util.keepScreenOn 10 | 11 | @Composable 12 | fun AmbientPlayerListener(player: Player) { 13 | val context = LocalContext.current 14 | DisposableEffect(player) { 15 | val listener = 16 | object : Listener { 17 | override fun onIsPlayingChanged(isPlaying: Boolean) { 18 | context.findActivity()?.keepScreenOn(isPlaying) 19 | } 20 | } 21 | player.addListener(listener) 22 | onDispose { 23 | player.removeListener(listener) 24 | context.findActivity()?.keepScreenOn(false) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/KeyIdentifier.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.ui.components.playback 2 | 3 | import androidx.compose.ui.input.key.Key 4 | import androidx.compose.ui.input.key.KeyEvent 5 | import androidx.compose.ui.input.key.key 6 | 7 | fun isDpad(event: KeyEvent): Boolean = 8 | event.key == Key.DirectionCenter || 9 | event.key == Key.DirectionUp || 10 | event.key == Key.DirectionDown || 11 | event.key == Key.DirectionLeft || 12 | event.key == Key.DirectionRight || 13 | event.key == Key.DirectionUpRight || 14 | event.key == Key.DirectionUpLeft || 15 | event.key == Key.DirectionDownRight || 16 | event.key == Key.DirectionDownLeft 17 | 18 | fun isMedia(event: KeyEvent): Boolean = 19 | event.key == Key.MediaPlay || 20 | event.key == Key.MediaPause || 21 | event.key == Key.MediaPlayPause || 22 | event.key == Key.MediaFastForward || 23 | event.key == Key.MediaSkipForward || 24 | event.key == Key.MediaRewind || 25 | event.key == Key.MediaSkipBackward || 26 | event.key == Key.MediaNext || 27 | event.key == Key.MediaPrevious 28 | 29 | fun isBackwardButton(event: KeyEvent): Boolean = 30 | event.key == Key.PageUp || 31 | event.key == Key.ChannelUp || 32 | event.key == Key.MediaPrevious || 33 | event.key == Key.MediaRewind || 34 | event.key == Key.MediaSkipBackward 35 | 36 | fun isForwardButton(event: KeyEvent): Boolean = 37 | event.key == Key.PageDown || 38 | event.key == Key.ChannelDown || 39 | event.key == Key.MediaNext || 40 | event.key == Key.MediaFastForward || 41 | event.key == Key.MediaSkipForward 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/PlaybackState.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.ui.components.playback 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.mutableStateOf 5 | 6 | @Composable 7 | fun rememberPlaybackState(player: PlayerControls) { 8 | } 9 | 10 | class PlaybackState( 11 | player: PlayerControls, 12 | ) { 13 | var isPlaying = mutableStateOf<Boolean>(false) 14 | private set 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/SeekBarState.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.ui.components.playback 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.setValue 9 | import androidx.media3.common.Player 10 | import androidx.media3.common.listen 11 | import androidx.media3.common.util.UnstableApi 12 | import com.github.damontecres.stashapp.util.launchIO 13 | import kotlinx.coroutines.CoroutineScope 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.Job 16 | import kotlinx.coroutines.delay 17 | import kotlinx.coroutines.withContext 18 | 19 | @UnstableApi 20 | @Composable 21 | fun rememberSeekBarState( 22 | player: Player, 23 | scope: CoroutineScope, 24 | ): SeekBarState { 25 | val seekBarState = remember(player) { SeekBarState(player, scope) } 26 | LaunchedEffect(player) { 27 | seekBarState.observe() 28 | } 29 | return seekBarState 30 | } 31 | 32 | @UnstableApi 33 | class SeekBarState( 34 | private val player: Player, 35 | private val scope: CoroutineScope, 36 | ) { 37 | var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_SEEK_FORWARD)) 38 | private set 39 | 40 | private var job: Job? = null 41 | 42 | fun onValueChange(progress: Float) { 43 | job?.cancel() 44 | job = 45 | scope.launchIO { 46 | delay(750L) 47 | withContext(Dispatchers.Main) { 48 | player.seekTo((player.duration * progress).toLong()) 49 | } 50 | } 51 | } 52 | 53 | suspend fun observe(): Nothing = 54 | player.listen { events -> 55 | if (events.contains(Player.EVENT_AVAILABLE_COMMANDS_CHANGED)) { 56 | isEnabled = isCommandAvailable(Player.COMMAND_SEEK_FORWARD) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/ui/util/CoilPreviewTransformation.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.ui.util 2 | 3 | import android.graphics.Bitmap 4 | import androidx.core.graphics.scale 5 | import coil3.size.Size 6 | import coil3.size.pxOrElse 7 | import coil3.transform.Transformation 8 | import com.github.damontecres.stashapp.util.StashPreviewLoader.GlideThumbnailTransformation.Companion.MAX_COLUMNS 9 | import com.github.damontecres.stashapp.util.StashPreviewLoader.GlideThumbnailTransformation.Companion.MAX_LINES 10 | 11 | class CoilPreviewTransformation( 12 | val targetWidth: Int, 13 | val targetHeight: Int, 14 | duration: Long, 15 | position: Long, 16 | ) : Transformation() { 17 | private val x: Int 18 | private val y: Int 19 | 20 | init { 21 | val square = position / (duration / (MAX_LINES * MAX_COLUMNS)) 22 | y = square.toInt() / MAX_LINES 23 | x = square.toInt() % MAX_COLUMNS 24 | } 25 | 26 | override val cacheKey: String 27 | get() = "CoilPreviewTransformation_$x,$y" 28 | 29 | override suspend fun transform( 30 | input: Bitmap, 31 | size: Size, 32 | ): Bitmap { 33 | val width = input.width / MAX_COLUMNS 34 | val height = input.height / MAX_LINES 35 | // Log.d(TAG, "input.width=${input.width}, input.height=${input.height}, width=$width, height=$height, size=$size") 36 | return Bitmap 37 | .createBitmap(input, x * width, y * height, width, height) 38 | .scale(size.width.pxOrElse { targetWidth }, size.height.pxOrElse { targetHeight }) 39 | } 40 | 41 | companion object { 42 | private const val TAG = "CoilPreviewTransformation" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/ui/util/CrossFadeFactory.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.ui.util 2 | 3 | import coil3.request.ImageResult 4 | import coil3.request.SuccessResult 5 | import coil3.transition.CrossfadeTransition 6 | import coil3.transition.Transition 7 | import coil3.transition.TransitionTarget 8 | import kotlin.time.Duration 9 | import kotlin.time.DurationUnit 10 | 11 | /** 12 | * Similar to [coil3.transition.CrossfadeTransition.Factory] but always cross fades even if loading from memory 13 | */ 14 | class CrossFadeFactory( 15 | val duration: Duration, 16 | ) : Transition.Factory { 17 | override fun create( 18 | target: TransitionTarget, 19 | result: ImageResult, 20 | ): Transition = 21 | if (result is SuccessResult) { 22 | CrossfadeTransition(target, result, duration.toInt(DurationUnit.MILLISECONDS), false) 23 | } else { 24 | Transition.Factory.NONE.create(target, result) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/ui/util/OneTimeLaunchedEffect.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.ui.util 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.saveable.rememberSaveable 8 | import androidx.compose.runtime.setValue 9 | import kotlinx.coroutines.CoroutineScope 10 | 11 | @Composable 12 | fun OneTimeLaunchedEffect( 13 | condition: Boolean, 14 | runOnceBlock: suspend CoroutineScope.() -> Unit, 15 | elseBlock: (suspend CoroutineScope.() -> Unit)? = null, 16 | ) { 17 | TODO("Work in progress") 18 | var hasRun by rememberSaveable { mutableStateOf(false) } 19 | if (!hasRun && condition) { 20 | LaunchedEffect(Unit) { 21 | runOnceBlock.invoke(this) 22 | hasRun = true 23 | } 24 | } else if (hasRun && elseBlock != null) { 25 | LaunchedEffect(Unit, elseBlock) 26 | } 27 | } 28 | 29 | /** 30 | * Run a [LaunchedEffect] exactly once even with multiple recompositions. 31 | * 32 | * If the composition is removed from the navigation back stack and "re-added", this will run again 33 | */ 34 | @Composable 35 | fun OneTimeLaunchedEffect(runOnceBlock: suspend CoroutineScope.() -> Unit) { 36 | var hasRun by rememberSaveable { mutableStateOf(false) } 37 | if (!hasRun) { 38 | LaunchedEffect(Unit) { 39 | hasRun = true 40 | runOnceBlock.invoke(this) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/AndroidExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | import com.github.damontecres.stashapp.data.DataType 4 | import com.github.damontecres.stashapp.data.DataType.GALLERY 5 | import com.github.damontecres.stashapp.data.DataType.GROUP 6 | import com.github.damontecres.stashapp.data.DataType.IMAGE 7 | import com.github.damontecres.stashapp.data.DataType.MARKER 8 | import com.github.damontecres.stashapp.data.DataType.PERFORMER 9 | import com.github.damontecres.stashapp.data.DataType.SCENE 10 | import com.github.damontecres.stashapp.data.DataType.STUDIO 11 | import com.github.damontecres.stashapp.data.DataType.TAG 12 | 13 | val DataType.defaultCardWidth 14 | get() = 15 | when (this) { 16 | SCENE -> 345 17 | GROUP -> 250 18 | MARKER -> 345 19 | PERFORMER -> 254 // 2/3 of height 20 | STUDIO -> 327 21 | TAG -> 250 22 | IMAGE -> 345 23 | GALLERY -> 345 24 | } 25 | 26 | val DataType.defaultCardHeight 27 | get() = 28 | when (this) { 29 | SCENE -> 194 30 | GROUP -> 250 31 | MARKER -> 194 32 | PERFORMER -> 381 33 | STUDIO -> 184 34 | TAG -> 250 35 | IMAGE -> 258 36 | GALLERY -> 258 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/Comparators.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | import androidx.leanback.widget.DiffCallback 4 | import androidx.recyclerview.widget.DiffUtil 5 | import com.github.damontecres.stashapp.api.fragment.StashData 6 | 7 | object StashComparator : DiffUtil.ItemCallback<StashData>() { 8 | override fun areItemsTheSame( 9 | oldItem: StashData, 10 | newItem: StashData, 11 | ): Boolean = oldItem::class == newItem::class && oldItem.id == newItem.id 12 | 13 | override fun areContentsTheSame( 14 | oldItem: StashData, 15 | newItem: StashData, 16 | ): Boolean = oldItem::class == newItem::class && oldItem == newItem 17 | } 18 | 19 | object StashDiffCallback : DiffCallback<StashData>() { 20 | override fun areItemsTheSame( 21 | oldItem: StashData, 22 | newItem: StashData, 23 | ): Boolean = StashComparator.areItemsTheSame(oldItem, newItem) 24 | 25 | override fun areContentsTheSame( 26 | oldItem: StashData, 27 | newItem: StashData, 28 | ): Boolean = StashComparator.areContentsTheSame(oldItem, newItem) 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/CreateNew.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | import com.github.damontecres.stashapp.data.DataType 4 | 5 | data class CreateNew( 6 | val dataType: DataType, 7 | val name: String, 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/DefaultKeyEventCallback.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | import android.view.KeyEvent 4 | 5 | interface DefaultKeyEventCallback : KeyEvent.Callback { 6 | override fun onKeyDown( 7 | keyCode: Int, 8 | event: KeyEvent, 9 | ): Boolean = false 10 | 11 | override fun onKeyUp( 12 | keyCode: Int, 13 | event: KeyEvent, 14 | ): Boolean = false 15 | 16 | override fun onKeyLongPress( 17 | keyCode: Int, 18 | event: KeyEvent, 19 | ): Boolean = false 20 | 21 | override fun onKeyMultiple( 22 | keyCode: Int, 23 | count: Int, 24 | event: KeyEvent, 25 | ): Boolean = false 26 | } 27 | 28 | interface DelegateKeyEventCallback : KeyEvent.Callback { 29 | val keyEventDelegate: KeyEvent.Callback? 30 | 31 | override fun onKeyDown( 32 | keyCode: Int, 33 | event: KeyEvent, 34 | ): Boolean = keyEventDelegate?.onKeyDown(keyCode, event) ?: false 35 | 36 | override fun onKeyUp( 37 | keyCode: Int, 38 | event: KeyEvent, 39 | ): Boolean = keyEventDelegate?.onKeyUp(keyCode, event) ?: false 40 | 41 | override fun onKeyLongPress( 42 | keyCode: Int, 43 | event: KeyEvent, 44 | ): Boolean = keyEventDelegate?.onKeyLongPress(keyCode, event) ?: false 45 | 46 | override fun onKeyMultiple( 47 | keyCode: Int, 48 | count: Int, 49 | event: KeyEvent, 50 | ): Boolean = keyEventDelegate?.onKeyMultiple(keyCode, count, event) ?: false 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/KeyEventDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | import android.view.KeyEvent 4 | 5 | /** 6 | * Fragments that implement this will receiving the events from the root activity 7 | */ 8 | interface KeyEventDispatcher { 9 | fun dispatchKeyEvent(event: KeyEvent): Boolean 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/LongClickPreference.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import androidx.preference.Preference 7 | import androidx.preference.PreferenceViewHolder 8 | 9 | class LongClickPreference( 10 | context: Context, 11 | attrs: AttributeSet?, 12 | ) : Preference(context, attrs) { 13 | var longClickListener: View.OnLongClickListener? = null 14 | 15 | override fun onBindViewHolder(holder: PreferenceViewHolder) { 16 | super.onBindViewHolder(holder) 17 | holder.itemView.setOnLongClickListener { 18 | longClickListener?.onLongClick(it) ?: false 19 | } 20 | } 21 | 22 | fun setOnLongClickListener(longClickListener: View.OnLongClickListener?) { 23 | this.longClickListener = longClickListener 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/MappedList.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | /** 4 | * A list that lazily transforms items on [get] 5 | */ 6 | class MappedList<T, R>( 7 | private val sourceList: List<T>, 8 | private val transform: (Int, T) -> R, 9 | ) : AbstractList<R>() { 10 | constructor( 11 | sourceList: List<T>, 12 | transform: (T) -> R, 13 | ) : this(sourceList, { _, x: T -> transform(x) }) 14 | 15 | override val size: Int 16 | get() = sourceList.size 17 | 18 | override fun get(index: Int): R = transform(index, sourceList[index]) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/PageFilterKey.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | import com.github.damontecres.stashapp.data.DataType 4 | 5 | /** 6 | * List of keys to look up how to sort items in a tab on a page 7 | */ 8 | enum class PageFilterKey( 9 | val dataType: DataType, 10 | val prefKey: String, 11 | ) { 12 | TAG_MARKERS(DataType.MARKER, "tag_markers"), 13 | TAG_GALLERIES(DataType.GALLERY, "tag_galleries"), 14 | TAG_SCENES(DataType.SCENE, "tag_scenes"), 15 | TAG_IMAGES(DataType.IMAGE, "tag_images"), 16 | TAG_PERFORMERS(DataType.PERFORMER, "tag_performers"), 17 | 18 | PERFORMER_SCENES(DataType.SCENE, "performer_scenes"), 19 | PERFORMER_GALLERIES(DataType.GALLERY, "performer_galleries"), 20 | PERFORMER_IMAGES(DataType.IMAGE, "performer_images"), 21 | PERFORMER_GROUPS(DataType.GROUP, "performer_groups"), 22 | PERFORMER_APPEARS_WITH(DataType.PERFORMER, "performer_appears_with"), 23 | 24 | STUDIO_GALLERIES(DataType.GALLERY, "studio_galleries"), 25 | STUDIO_IMAGES(DataType.IMAGE, "studio_images"), 26 | 27 | GALLERY_IMAGES(DataType.IMAGE, "gallery_images"), 28 | 29 | STUDIO_SCENES(DataType.SCENE, "studio_scenes"), 30 | STUDIO_GROUPS(DataType.GROUP, "studio_groups"), 31 | STUDIO_PERFORMERS(DataType.PERFORMER, "studio_performers"), 32 | STUDIO_CHILDREN(DataType.STUDIO, "studio_children"), 33 | 34 | GROUP_SCENES(DataType.SCENE, "group_scenes"), 35 | GROUP_SUB_GROUPS(DataType.GROUP, "group_sub_groups"), 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/RemoveLongClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | import android.util.Log 4 | import android.widget.Toast 5 | import com.github.damontecres.stashapp.api.fragment.StashData 6 | import com.github.damontecres.stashapp.filter.extractTitle 7 | import com.github.damontecres.stashapp.presenters.StashPresenter 8 | import com.github.damontecres.stashapp.presenters.StashPresenter.PopUpItem.Companion.REMOVE_POPUP_ITEM 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.launch 11 | 12 | fun <T : StashData> createRemoveLongClickListener( 13 | scope: () -> CoroutineScope, 14 | rowManager: ListRowManager<T>, 15 | ): StashPresenter.LongClickCallBack<T> = 16 | StashPresenter 17 | .LongClickCallBack<T>( 18 | StashPresenter.PopUpItem.DEFAULT to StashPresenter.PopUpAction { cardView, _ -> cardView.performClick() }, 19 | ).addAction(REMOVE_POPUP_ITEM, { readOnlyModeDisabled() }) { cardView, item -> 20 | if (readOnlyModeDisabled()) { 21 | scope.invoke().launch(StashCoroutineExceptionHandler(autoToast = true)) { 22 | Log.v( 23 | "RemoveLongClickListener", 24 | "Removing id=${item.id} (${item::class.simpleName})", 25 | ) 26 | if (rowManager.remove(item)) { 27 | val name = extractTitle(item) 28 | Toast.makeText(cardView.context, "Removed '$name'", Toast.LENGTH_SHORT).show() 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/SingleItemObjectAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | import androidx.leanback.widget.ObjectAdapter 4 | import com.github.damontecres.stashapp.presenters.StashPresenter 5 | 6 | class SingleItemObjectAdapter<T : Any>( 7 | presenter: StashPresenter<T>, 8 | ) : ObjectAdapter(presenter) { 9 | constructor(presenter: StashPresenter<T>, item: T) : this(presenter) { 10 | this.item = item 11 | } 12 | 13 | var item: T? = null 14 | set(newValue) { 15 | field = newValue 16 | notifyChanged() 17 | } 18 | 19 | override fun size(): Int = if (item == null) 0 else 1 20 | 21 | override fun get(position: Int): Any? { 22 | if (position != 0) { 23 | throw IllegalArgumentException() 24 | } 25 | return item 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/SkipParams.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | sealed interface SkipParams { 4 | data object Default : SkipParams 5 | 6 | data class Values( 7 | val skipForward: Long, 8 | val skipBack: Long, 9 | ) : SkipParams 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/StashCoroutineExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | import android.util.Log 4 | import android.widget.Toast 5 | import com.github.damontecres.stashapp.StashApplication 6 | import kotlinx.coroutines.CoroutineExceptionHandler 7 | import kotlin.coroutines.CoroutineContext 8 | 9 | /** 10 | * A general [CoroutineExceptionHandler] which can optionally show [Toast]s when an exception is thrown 11 | * 12 | * Note: a toast will be shown for each parameter given, up to three! 13 | * 14 | * @param autoToast automatically show a toast with the exception's message 15 | * @param toast the toast to show when an exception occurs 16 | * @param makeToast make a toast to show when an exception occurs 17 | */ 18 | class StashCoroutineExceptionHandler( 19 | private val autoToast: Boolean = false, 20 | private val toast: Toast? = null, 21 | private val makeToast: ((Throwable) -> Toast)? = null, 22 | ) : CoroutineExceptionHandler { 23 | constructor( 24 | toast: Toast? = null, 25 | makeToast: ((Throwable) -> Toast)? = null, 26 | ) : this(false, toast, makeToast) 27 | 28 | override val key: CoroutineContext.Key<*> 29 | get() = CoroutineExceptionHandler 30 | 31 | override fun handleException( 32 | context: CoroutineContext, 33 | exception: Throwable, 34 | ) { 35 | Log.e(TAG, "Exception in coroutine", exception) 36 | toast?.show() 37 | makeToast?.let { it(exception).show() } 38 | if (autoToast) { 39 | Toast 40 | .makeText( 41 | StashApplication.getApplication(), 42 | "Error: ${exception.message}", 43 | Toast.LENGTH_LONG, 44 | ).show() 45 | } 46 | } 47 | 48 | companion object { 49 | const val TAG = "StashCoroutineExceptionHandler" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/StashFragmentPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.fragment.app.FragmentStatePagerAdapter 6 | import com.github.damontecres.stashapp.StashApplication 7 | import com.github.damontecres.stashapp.data.DataType 8 | 9 | /** 10 | * A [FragmentStatePagerAdapter] to show various tabs for data types 11 | */ 12 | class StashFragmentPagerAdapter( 13 | private val items: List<PagerEntry>, 14 | fm: FragmentManager, 15 | ) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { 16 | var fragmentCreatedListener: ((Fragment, Int) -> Unit)? = null 17 | 18 | override fun getCount(): Int = items.size 19 | 20 | override fun getItem(position: Int): Fragment { 21 | val newFragment = items[position].createFragment.invoke() 22 | fragmentCreatedListener?.invoke(newFragment, position) 23 | return newFragment 24 | } 25 | 26 | override fun getPageTitle(position: Int): CharSequence = items[position].title 27 | 28 | /** 29 | * Represents a tab with an title 30 | */ 31 | data class PagerEntry( 32 | val title: String, 33 | val createFragment: () -> Fragment, 34 | ) { 35 | constructor(dataType: DataType, createFragment: () -> Fragment) : this( 36 | StashApplication.getApplication().getString(dataType.pluralStringId), 37 | createFragment, 38 | ) 39 | } 40 | 41 | companion object { 42 | private const val TAG = "StashFragmentPagerAdapter" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/svg/SvgDecoder.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util.svg 2 | 3 | import com.bumptech.glide.load.Options 4 | import com.bumptech.glide.load.ResourceDecoder 5 | import com.bumptech.glide.load.engine.Resource 6 | import com.bumptech.glide.load.resource.SimpleResource 7 | import com.bumptech.glide.request.target.Target.SIZE_ORIGINAL 8 | import com.caverock.androidsvg.SVG 9 | import com.caverock.androidsvg.SVGParseException 10 | import java.io.IOException 11 | import java.io.InputStream 12 | 13 | // From https://github.com/bumptech/glide/tree/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg 14 | class SvgDecoder : ResourceDecoder<InputStream, SVG> { 15 | override fun handles( 16 | source: InputStream, 17 | options: Options, 18 | ): Boolean { 19 | // TODO: Can we tell? 20 | return true 21 | } 22 | 23 | @Throws(IOException::class) 24 | override fun decode( 25 | source: InputStream, 26 | width: Int, 27 | height: Int, 28 | options: Options, 29 | ): Resource<SVG> = 30 | try { 31 | val svg = SVG.getFromInputStream(source) 32 | if (width != SIZE_ORIGINAL) { 33 | svg.documentWidth = width.toFloat() 34 | } 35 | if (height != SIZE_ORIGINAL) { 36 | svg.documentHeight = height.toFloat() 37 | } 38 | SimpleResource(svg) 39 | } catch (ex: SVGParseException) { 40 | throw IOException("Cannot load SVG from stream", ex) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/svg/SvgDrawableTranscoder.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util.svg 2 | 3 | import android.graphics.drawable.PictureDrawable 4 | import com.bumptech.glide.load.Options 5 | import com.bumptech.glide.load.engine.Resource 6 | import com.bumptech.glide.load.resource.SimpleResource 7 | import com.bumptech.glide.load.resource.transcode.ResourceTranscoder 8 | import com.caverock.androidsvg.SVG 9 | 10 | // From https://github.com/bumptech/glide/tree/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg 11 | class SvgDrawableTranscoder : ResourceTranscoder<SVG, PictureDrawable> { 12 | override fun transcode( 13 | toTranscode: Resource<SVG>, 14 | options: Options, 15 | ): Resource<PictureDrawable> { 16 | val svg = toTranscode.get() 17 | val picture = svg.renderToPicture() 18 | val drawable = PictureDrawable(picture) 19 | return SimpleResource(drawable) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/util/svg/SvgSoftwareLayerSetter.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.util.svg 2 | 3 | import android.graphics.drawable.PictureDrawable 4 | import android.widget.ImageView 5 | import com.bumptech.glide.load.DataSource 6 | import com.bumptech.glide.load.engine.GlideException 7 | import com.bumptech.glide.request.RequestListener 8 | import com.bumptech.glide.request.target.ImageViewTarget 9 | import com.bumptech.glide.request.target.Target 10 | 11 | // From https://github.com/bumptech/glide/tree/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg 12 | class SvgSoftwareLayerSetter : RequestListener<PictureDrawable> { 13 | override fun onLoadFailed( 14 | e: GlideException?, 15 | model: Any?, 16 | target: Target<PictureDrawable>, 17 | isFirstResource: Boolean, 18 | ): Boolean { 19 | val view = (target as ImageViewTarget<*>).view 20 | view.setLayerType(ImageView.LAYER_TYPE_NONE, null) 21 | return false 22 | } 23 | 24 | override fun onResourceReady( 25 | resource: PictureDrawable, 26 | model: Any, 27 | target: Target<PictureDrawable>, 28 | dataSource: DataSource, 29 | isFirstResource: Boolean, 30 | ): Boolean { 31 | val view = (target as ImageViewTarget<*>).view 32 | view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null) 33 | return false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/FontSpan.kt: -------------------------------------------------------------------------------- 1 | 2 | package com.github.damontecres.stashapp.views 3 | 4 | import android.graphics.Typeface 5 | import android.text.TextPaint 6 | import android.text.style.MetricAffectingSpan 7 | 8 | /** 9 | * Apply a font to a span 10 | */ 11 | class FontSpan( 12 | private val font: Typeface, 13 | ) : MetricAffectingSpan() { 14 | override fun updateMeasureState(textPaint: TextPaint) = setFont(textPaint) 15 | 16 | override fun updateDrawState(textPaint: TextPaint) = setFont(textPaint) 17 | 18 | private fun setFont(textPaint: TextPaint) { 19 | textPaint.apply { 20 | // Set the font and use the current style if available 21 | typeface = Typeface.create(font, typeface?.style ?: Typeface.NORMAL) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/HomeImageButton.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.appcompat.content.res.AppCompatResources 6 | import androidx.appcompat.widget.AppCompatImageButton 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.findFragment 9 | import androidx.lifecycle.ViewModelProvider 10 | import com.github.damontecres.stashapp.R 11 | import com.github.damontecres.stashapp.views.models.ServerViewModel 12 | 13 | /** 14 | * An [AppCompatImageButton] that is the stash server icon and clicking returns to the main page 15 | */ 16 | class HomeImageButton( 17 | context: Context, 18 | attrs: AttributeSet?, 19 | ) : AppCompatImageButton(context, attrs) { 20 | private val serverViewModel by lazy { 21 | ViewModelProvider(findFragment<Fragment>().requireActivity())[ServerViewModel::class] 22 | } 23 | 24 | init { 25 | setOnClickListener { 26 | serverViewModel.navigationManager.goToMain() 27 | } 28 | setBackgroundDrawable(AppCompatResources.getDrawable(context, R.drawable.icon_button_selector)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/LoadingFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views 2 | 3 | import androidx.fragment.app.Fragment 4 | import com.github.damontecres.stashapp.R 5 | 6 | /** 7 | * Just displays a ContentLoadingProgressBar indefinitely 8 | */ 9 | class LoadingFragment : Fragment(R.layout.loading_fragment) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/StarRatingBar.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.KeyEvent 6 | import androidx.appcompat.widget.AppCompatRatingBar 7 | 8 | /** 9 | * Overrides [AppCompatRatingBar] to allow for wrapping around left or right 10 | */ 11 | class StarRatingBar( 12 | context: Context, 13 | attrs: AttributeSet?, 14 | ) : AppCompatRatingBar(context, attrs) { 15 | override fun onKeyDown( 16 | keyCode: Int, 17 | event: KeyEvent?, 18 | ): Boolean { 19 | if (super.isEnabled()) { 20 | if ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_MINUS) && rating < .05f) { 21 | rating = 5f 22 | return true 23 | } else if ((keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_PLUS) && rating > 4.95f) { 24 | rating = 0f 25 | return true 26 | } 27 | } 28 | return super.onKeyDown(keyCode, event) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/StashOnFocusChangeListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import androidx.annotation.FractionRes 6 | 7 | /** 8 | * A [View.OnFocusChangeListener] which slightly zooms the view 9 | */ 10 | open class StashOnFocusChangeListener( 11 | val context: Context, 12 | @FractionRes fraction: Int = androidx.leanback.R.fraction.lb_search_orb_focused_zoom, 13 | ) : View.OnFocusChangeListener { 14 | private val mFocusedZoom = context.resources.getFraction(fraction, 1, 1) 15 | 16 | private val mScaleDurationMs = 17 | context.resources.getInteger( 18 | androidx.leanback.R.integer.lb_search_orb_scale_duration_ms, 19 | ) 20 | 21 | override fun onFocusChange( 22 | v: View, 23 | hasFocus: Boolean, 24 | ) { 25 | val zoom = if (hasFocus) mFocusedZoom else 1f 26 | v 27 | .animate() 28 | .scaleX(zoom) 29 | .scaleY(zoom) 30 | .setDuration(mScaleDurationMs.toLong()) 31 | .start() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/StashZoomImageView.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.Drawable 5 | import android.util.AttributeSet 6 | import android.util.Log 7 | import androidx.annotation.AttrRes 8 | import com.otaliastudios.zoom.ZoomImageView 9 | 10 | /** 11 | * A small wrapper around [ZoomImageView] to ensure when replacing the image the zoom is reset 12 | */ 13 | class StashZoomImageView private constructor( 14 | context: Context, 15 | attrs: AttributeSet? = null, 16 | @AttrRes defStyleAttr: Int = 0, 17 | ) : ZoomImageView(context, attrs, defStyleAttr) { 18 | constructor( 19 | context: Context, 20 | attrs: AttributeSet? = null, 21 | ) : this(context, attrs, 0) 22 | 23 | override fun setImageDrawable(drawable: Drawable?) { 24 | Log.v(TAG, "setImageDrawable: drawable=$drawable") 25 | super.setImageDrawable(drawable) 26 | moveToCenter(1.0f, false) 27 | } 28 | 29 | companion object { 30 | private const val TAG = "StashZoomImageView" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/TabbedGridTitleView.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.widget.Button 7 | import androidx.constraintlayout.widget.ConstraintLayout 8 | import androidx.leanback.widget.TitleViewAdapter 9 | import com.github.damontecres.stashapp.R 10 | 11 | class TabbedGridTitleView( 12 | context: Context, 13 | attrs: AttributeSet? = null, 14 | ) : ConstraintLayout(context, attrs), 15 | TitleViewAdapter.Provider { 16 | override fun getTitleViewAdapter(): TitleViewAdapter = 17 | object : TitleViewAdapter() { 18 | override fun getSearchAffordanceView(): View = findViewById<Button>(R.id.sort_button) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/TitleTransitionHelper.kt: -------------------------------------------------------------------------------- 1 | // Adapted from https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:leanback/leanback/src/main/java/androidx/leanback/widget/TitleHelper.java 2 | package com.github.damontecres.stashapp.views 3 | 4 | import android.transition.Scene 5 | import android.transition.Transition 6 | import android.transition.TransitionInflater 7 | import android.transition.TransitionManager 8 | import android.view.View 9 | import android.view.ViewGroup 10 | 11 | class TitleTransitionHelper( 12 | val sceneRoot: ViewGroup, 13 | val titleView: View, 14 | ) { 15 | private val mTitleUpTransition: Transition 16 | private val mTitleDownTransition: Transition 17 | private val mSceneWithTitle: Scene 18 | private val mSceneWithoutTitle: Scene 19 | 20 | init { 21 | mTitleUpTransition = 22 | TransitionInflater 23 | .from(sceneRoot.context) 24 | .inflateTransition(androidx.leanback.R.transition.lb_title_out) 25 | 26 | mTitleDownTransition = 27 | TransitionInflater 28 | .from(sceneRoot.context) 29 | .inflateTransition(androidx.leanback.R.transition.lb_title_in) 30 | 31 | val sceneTitle = Scene(sceneRoot) 32 | sceneTitle.setEnterAction { 33 | titleView.visibility = View.VISIBLE 34 | } 35 | mSceneWithTitle = sceneTitle 36 | 37 | val sceneWithoutTitle = Scene(sceneRoot) 38 | sceneWithoutTitle.setEnterAction { 39 | titleView.visibility = View.GONE 40 | } 41 | mSceneWithoutTitle = sceneWithoutTitle 42 | } 43 | 44 | /** 45 | * Shows the title. 46 | */ 47 | fun showTitle(show: Boolean) { 48 | if (show) { 49 | TransitionManager.go(mSceneWithTitle, mTitleDownTransition) 50 | } else { 51 | TransitionManager.go(mSceneWithoutTitle, mTitleUpTransition) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/WrapAroundSeekBar.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.util.AttributeSet 6 | import android.view.KeyEvent 7 | import androidx.appcompat.widget.AppCompatSeekBar 8 | 9 | /** 10 | * Overrides [AppCompatSeekBar] to allow for wrapping around left or right 11 | */ 12 | class WrapAroundSeekBar( 13 | context: Context, 14 | attrs: AttributeSet?, 15 | ) : AppCompatSeekBar(context, attrs) { 16 | override fun onKeyDown( 17 | keyCode: Int, 18 | event: KeyEvent?, 19 | ): Boolean { 20 | if (super.isEnabled()) { 21 | val minimum = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) min else 0 22 | if ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_MINUS) && progress <= minimum) { 23 | progress = max 24 | return true 25 | } else if ((keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_PLUS) && progress >= max) { 26 | progress = minimum 27 | return true 28 | } 29 | } 30 | return super.onKeyDown(keyCode, event) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/dialog/ConfirmationDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views.dialog 2 | 3 | import android.app.Dialog 4 | import android.content.DialogInterface 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AlertDialog 7 | import androidx.fragment.app.DialogFragment 8 | import androidx.fragment.app.FragmentManager 9 | import com.github.damontecres.stashapp.R 10 | 11 | /** 12 | * A simple dialog to confirm or cancel an action 13 | */ 14 | class ConfirmationDialogFragment( 15 | private val message: CharSequence, 16 | private val onClickListener: DialogInterface.OnClickListener, 17 | ) : DialogFragment() { 18 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = 19 | AlertDialog 20 | .Builder(requireContext()) 21 | .setMessage(message) 22 | .setPositiveButton(getString(R.string.stashapp_actions_confirm), onClickListener) 23 | .setNegativeButton(getString(R.string.stashapp_actions_cancel), onClickListener) 24 | .create() 25 | 26 | override fun onResume() { 27 | super.onResume() 28 | (dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_NEGATIVE)?.requestFocus() 29 | } 30 | 31 | companion object { 32 | fun show( 33 | fm: FragmentManager, 34 | message: CharSequence, 35 | onConfirm: () -> Unit, 36 | ) { 37 | ConfirmationDialogFragment(message) { _, which -> 38 | if (which == DialogInterface.BUTTON_POSITIVE) { 39 | onConfirm.invoke() 40 | } 41 | }.show(fm, null) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/models/CardUiSettings.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views.models 2 | 3 | /** 4 | * Basic UI settings that affect the cards 5 | */ 6 | data class CardUiSettings( 7 | val maxSearchResults: Int, 8 | val playVideoPreviews: Boolean, 9 | val videoPreviewAudio: Boolean, 10 | val columns: Int, 11 | val showRatings: Boolean, 12 | val imageCrop: Boolean, 13 | val videoDelay: Int, 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/models/EqualityMutableLiveData.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views.models 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | 5 | /** 6 | * A [MutableLiveData] that only notifies observers if the value does not equal the previous value 7 | */ 8 | class EqualityMutableLiveData<T> : MutableLiveData<T> { 9 | constructor() : super() 10 | constructor(value: T) : super(value) 11 | 12 | override fun setValue(value: T?) { 13 | if (value != getValue()) { 14 | super.setValue(value) 15 | } 16 | } 17 | 18 | fun setValueNoCheck(value: T?) { 19 | super.setValue(value) 20 | } 21 | 22 | override fun postValue(value: T?) { 23 | if (value != getValue()) { 24 | super.postValue(value) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/models/GalleryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views.models 2 | 3 | import com.github.damontecres.stashapp.api.fragment.GalleryData 4 | import com.github.damontecres.stashapp.util.QueryEngine 5 | 6 | class GalleryViewModel : ItemViewModel<GalleryData>() { 7 | override suspend fun fetch( 8 | queryEngine: QueryEngine, 9 | id: String, 10 | ): GalleryData? = queryEngine.getGallery(id) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/models/GroupViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views.models 2 | 3 | import com.github.damontecres.stashapp.api.fragment.GroupData 4 | import com.github.damontecres.stashapp.util.QueryEngine 5 | 6 | class GroupViewModel : ItemViewModel<GroupData>() { 7 | override suspend fun fetch( 8 | queryEngine: QueryEngine, 9 | id: String, 10 | ): GroupData? = queryEngine.getGroup(id) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/models/ItemViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views.models 2 | 3 | import android.os.Bundle 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.github.damontecres.stashapp.api.fragment.StashData 8 | import com.github.damontecres.stashapp.navigation.Destination 9 | import com.github.damontecres.stashapp.util.QueryEngine 10 | import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler 11 | import com.github.damontecres.stashapp.util.StashServer 12 | import com.github.damontecres.stashapp.util.getDestination 13 | import kotlinx.coroutines.launch 14 | 15 | /** 16 | * Base [ViewModel] for simple [StashData] items 17 | */ 18 | abstract class ItemViewModel<T : StashData> : ViewModel() { 19 | private val _item = EqualityMutableLiveData<T>() 20 | val item: LiveData<T?> = _item 21 | 22 | lateinit var itemId: String 23 | 24 | /** 25 | * Fetch the item for the given id 26 | */ 27 | abstract suspend fun fetch( 28 | queryEngine: QueryEngine, 29 | id: String, 30 | ): T? 31 | 32 | /** 33 | * Initialize the [ViewModel] by fetching the item in the background and updating it 34 | */ 35 | fun init(args: Bundle) { 36 | itemId = args.getDestination<Destination.Item>().id 37 | viewModelScope.launch(StashCoroutineExceptionHandler(true)) { 38 | val queryEngine = QueryEngine(StashServer.requireCurrentServer()) 39 | val newValue = fetch(queryEngine, itemId) 40 | if (newValue == null) { 41 | _item.setValueNoCheck(null) 42 | } else { 43 | _item.value = newValue 44 | } 45 | } 46 | } 47 | 48 | fun update(item: T) { 49 | _item.value = item 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/models/PerformerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views.models 2 | 3 | import com.github.damontecres.stashapp.api.fragment.PerformerData 4 | import com.github.damontecres.stashapp.util.QueryEngine 5 | 6 | class PerformerViewModel : ItemViewModel<PerformerData>() { 7 | override suspend fun fetch( 8 | queryEngine: QueryEngine, 9 | id: String, 10 | ): PerformerData? = queryEngine.getPerformer(id) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/models/PlaybackViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views.models 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.github.damontecres.stashapp.data.Scene 8 | import com.github.damontecres.stashapp.util.QueryEngine 9 | import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler 10 | import com.github.damontecres.stashapp.util.StashServer 11 | import kotlinx.coroutines.launch 12 | 13 | class PlaybackViewModel : ViewModel() { 14 | private val _scene = MutableLiveData<Scene>() 15 | val scene: LiveData<Scene?> = _scene 16 | 17 | fun setScene(id: String) { 18 | viewModelScope.launch(StashCoroutineExceptionHandler(true)) { 19 | val queryEngine = QueryEngine(StashServer.requireCurrentServer()) 20 | val result = queryEngine.getVideoScene(id) 21 | _scene.value = result?.let { Scene.fromVideoSceneData(it) } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/models/StudioViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views.models 2 | 3 | import com.github.damontecres.stashapp.api.fragment.StudioData 4 | import com.github.damontecres.stashapp.util.QueryEngine 5 | 6 | class StudioViewModel : ItemViewModel<StudioData>() { 7 | override suspend fun fetch( 8 | queryEngine: QueryEngine, 9 | id: String, 10 | ): StudioData? = queryEngine.getStudio(id) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/models/TabbedGridViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views.models 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.SavedStateHandle 5 | import androidx.lifecycle.ViewModel 6 | import com.github.damontecres.stashapp.util.StashFragmentPagerAdapter.PagerEntry 7 | 8 | class TabbedGridViewModel( 9 | private val savedStateHandle: SavedStateHandle, 10 | ) : ViewModel() { 11 | val title: MutableLiveData<CharSequence?> = savedStateHandle.getLiveData("title", null) 12 | 13 | val tabs: MutableLiveData<List<PagerEntry>> = MutableLiveData() 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/damontecres/stashapp/views/models/TagViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp.views.models 2 | 3 | import com.github.damontecres.stashapp.api.fragment.TagData 4 | import com.github.damontecres.stashapp.util.QueryEngine 5 | 6 | class TagViewModel : ItemViewModel<TagData>() { 7 | override suspend fun fetch( 8 | queryEngine: QueryEngine, 9 | id: String, 10 | ): TagData? = queryEngine.getTag(id) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <set xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <translate android:fromXDelta="50%p" android:toXDelta="0" 4 | android:duration="@android:integer/config_mediumAnimTime"/> 5 | <alpha android:fromAlpha="0.0" android:toAlpha="1.0" 6 | android:duration="@android:integer/config_mediumAnimTime" /> 7 | </set> 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <set xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <translate android:fromXDelta="0" android:toXDelta="-50%p" 4 | android:duration="@android:integer/config_mediumAnimTime"/> 5 | <alpha android:fromAlpha="1.0" android:toAlpha="0" 6 | android:duration="@android:integer/config_mediumAnimTime" /> 7 | </set> 8 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_in.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:interpolator="@android:interpolator/accelerate_quad" 4 | android:valueFrom="0" 5 | android:valueTo="1" 6 | android:propertyName="alpha" 7 | android:duration="400" 8 | /> 9 | -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_out.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:interpolator="@android:interpolator/accelerate_quad" 4 | android:valueFrom="1.0" 5 | android:valueTo="0.0" 6 | android:propertyName="alpha" 7 | android:duration="400" 8 | /> 9 | -------------------------------------------------------------------------------- /app/src/main/res/color/clickable_text.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true" android:color="@color/selected_background"/> 4 | <item android:state_focused="true" android:color="@color/fastlane_background" /> 5 | <item android:color="@color/popup_selected_background" /> 6 | </selector> 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/filter_thumb.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true" android:color="@color/seek_bar"/> 4 | <item android:state_focused="true" android:color="@color/seek_bar" /> 5 | <item android:color="@color/popup_selected_background" /> 6 | </selector> 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/seek_bar_blue.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true" android:color="@android:color/holo_blue_dark"/> 4 | <item android:state_focused="true" android:color="@android:color/holo_blue_light" /> 5 | <item android:color="@android:color/holo_blue_dark" /> 6 | </selector> 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/seek_bar_green.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true" android:color="@android:color/holo_green_dark"/> 4 | <item android:state_focused="true" android:color="@android:color/holo_green_light" /> 5 | <item android:color="@android:color/holo_green_dark" /> 6 | </selector> 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/seek_bar_red.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true" android:color="@android:color/holo_red_dark"/> 4 | <item android:state_focused="true" android:color="@android:color/holo_red_light" /> 5 | <item android:color="@android:color/holo_red_dark" /> 6 | </selector> 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_add_box_24.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="128dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="128dp"> 2 | 3 | <path android:fillColor="@android:color/white" android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/> 4 | 5 | </vector> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_camera_indoor_48.xml: -------------------------------------------------------------------------------- 1 | <vector android:height="48dp" android:tint="#FFFFFF" 2 | android:viewportHeight="24" android:viewportWidth="24" 3 | android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> 4 | <path android:fillColor="@android:color/white" android:pathData="M12,3L4,9v12h16V9L12,3zM16,16.06L14,15v1c0,0.55 -0.45,1 -1,1H9c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h4c0.55,0 1,0.45 1,1v1l2,-1.06V16.06z"/> 5 | </vector> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_fast_forward_24.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="24dp" 3 | android:height="24dp" 4 | android:viewportWidth="24" 5 | android:viewportHeight="24" 6 | android:tint="?attr/colorControlNormal"> 7 | <path 8 | android:fillColor="@android:color/white" 9 | android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/> 10 | </vector> 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_fast_rewind_24.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="24dp" 3 | android:height="24dp" 4 | android:viewportWidth="24" 5 | android:viewportHeight="24" 6 | android:tint="?attr/colorControlNormal"> 7 | <path 8 | android:fillColor="@android:color/white" 9 | android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z"/> 10 | </vector> 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_more_vert_96.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="96dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="96dp"> 2 | 3 | <path android:fillColor="@android:color/white" android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/> 4 | 5 | </vector> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_pause_24.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="24dp" 3 | android:height="24dp" 4 | android:viewportWidth="24" 5 | android:viewportHeight="24" 6 | android:tint="?attr/colorControlNormal"> 7 | <path 8 | android:fillColor="@android:color/white" 9 | android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/> 10 | </vector> 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_play_arrow_24.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="24dp" 3 | android:height="24dp" 4 | android:viewportWidth="24" 5 | android:viewportHeight="24" 6 | android:tint="?attr/colorControlNormal"> 7 | <path 8 | android:fillColor="@android:color/white" 9 | android:pathData="M8,5v14l11,-7z"/> 10 | </vector> 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_skip_next_24.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="24dp" 3 | android:height="24dp" 4 | android:viewportWidth="24" 5 | android:viewportHeight="24" 6 | android:tint="?attr/colorControlNormal"> 7 | <path 8 | android:fillColor="@android:color/white" 9 | android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/> 10 | </vector> 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_skip_previous_24.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="24dp" 3 | android:height="24dp" 4 | android:viewportWidth="24" 5 | android:viewportHeight="24" 6 | android:tint="?attr/colorControlNormal"> 7 | <path 8 | android:fillColor="@android:color/white" 9 | android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"/> 10 | </vector> 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_stop_24.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="24dp" 3 | android:height="24dp" 4 | android:viewportWidth="24" 5 | android:viewportHeight="24" 6 | android:tint="?attr/colorControlNormal"> 7 | <path 8 | android:fillColor="@android:color/white" 9 | android:pathData="M6,6h12v12H6z"/> 10 | </vector> 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_undo_24.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> 2 | 3 | <path android:fillColor="@android:color/white" android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z"/> 4 | 5 | </vector> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_selector.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true"> 4 | <ripple android:color="@color/selected_background"/> 5 | </item> 6 | <item android:state_focused="true" android:drawable="@color/selected_background" /> 7 | <item android:drawable="@color/default_card_background"/> 8 | </selector> 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_selector_default_bg.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true"> 4 | <ripple android:color="@color/selected_background"/> 5 | </item> 6 | <item android:state_focused="true" android:drawable="@color/selected_background" /> 7 | <item android:drawable="@color/default_background"/> 8 | </selector> 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_selector_marker_picker.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true"> 4 | <ripple android:color="@color/selected_background"/> 5 | </item> 6 | <item android:state_focused="true" android:drawable="@color/selected_background_50" /> 7 | <item android:drawable="@color/transparent_black_25" /> 8 | </selector> 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/captions_svgrepo_com.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="200dp" android:viewportHeight="24" android:viewportWidth="24" android:width="200dp"> 2 | 3 | <path android:fillColor="#000000" android:pathData="M6,10v4c0,1.103 0.897,2 2,2h3v-2L8,14v-4h3L11,8L8,8c-1.103,0 -2,0.897 -2,2zM13,10v4c0,1.103 0.897,2 2,2h3v-2h-3v-4h3L18,8h-3c-1.103,0 -2,0.897 -2,2z"/> 4 | 5 | <path android:fillColor="#000000" android:pathData="M20,4H4c-1.103,0 -2,0.897 -2,2v12c0,1.103 0.897,2 2,2h16c1.103,0 2,-0.897 2,-2V6c0,-1.103 -0.897,-2 -2,-2zM4,18V6h16l0.002,12H4z"/> 6 | 7 | </vector> 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circular_arrow_right.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="800dp" 3 | android:height="800dp" 4 | android:viewportWidth="75.695" 5 | android:viewportHeight="75.695"> 6 | <!-- Modified from https://www.svgrepo.com/svg/168006/circular-arrow-clock under CC0 License --> 7 | <path 8 | android:pathData="M75.695,37.846c0,20.869 -16.98,37.85 -37.848,37.85C16.981,75.695 0,58.715 0,37.846C0,16.977 16.981,0 37.848,0c7.628,0 15.055,2.331 21.31,6.592l5.816,-5.817l4.679,17.946l-17.949,-4.678l4.069,-4.072c-5.319,-3.422 -11.538,-5.3 -17.929,-5.3c-18.294,0 -33.176,14.882 -33.176,33.174c0,18.294 14.882,33.178 33.176,33.178c18.293,0 33.175,-14.884 33.175,-33.178H75.695z" 9 | android:fillColor="#ffffff" /> 10 | </vector> 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/default_gallery.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="112.5dp" android:viewportHeight="720" android:viewportWidth="1280" android:width="200dp"> 2 | 3 | <path android:fillColor="#ffffff" android:pathData="M512,136c-35.3,0 -64,28.7 -64,64l0,224c0,35.3 28.7,64 64,64l352,0c35.3,0 64,-28.7 64,-64l0,-224c0,-35.3 -28.7,-64 -64,-64L512,136zM748,242.7l96,144c4.9,7.4 5.4,16.8 1.2,24.6S832.9,424 824,424l-144,0 -48,0 -80,0c-9.2,0 -17.6,-5.3 -21.6,-13.6s-2.9,-18.2 2.9,-25.4l64,-80c4.6,-5.7 11.4,-9 18.7,-9s14.2,3.3 18.7,9l17.3,21.6 56,-84C712.5,236 720,232 728,232s15.5,4 20,10.7zM544,232a32,32 0,1 1,64 0,32 32,0 1,1 -64,0zM400,224c0,-13.3 -10.7,-24 -24,-24S352,210.7 352,224L352,448c0,75.1 60.9,136 136,136l320,0c13.3,0 24,-10.7 24,-24s-10.7,-24 -24,-24l-320,0c-48.6,0 -88,-39.4 -88,-88l0,-224z"/> 4 | 5 | </vector> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/default_group.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="112.5dp" android:viewportHeight="720" android:viewportWidth="1280" android:width="200dp"> 2 | 3 | <path android:fillColor="#ffffff" android:pathData="M384,200C384,164.7 412.7,136 448,136l384,0c35.3,0 64,28.7 64,64l0,320c0,35.3 -28.7,64 -64,64L448,584c-35.3,0 -64,-28.7 -64,-64L384,200zM432,472l0,32c0,8.8 7.2,16 16,16l32,0c8.8,0 16,-7.2 16,-16l0,-32c0,-8.8 -7.2,-16 -16,-16l-32,0c-8.8,0 -16,7.2 -16,16zM800,456c-8.8,0 -16,7.2 -16,16l0,32c0,8.8 7.2,16 16,16l32,0c8.8,0 16,-7.2 16,-16l0,-32c0,-8.8 -7.2,-16 -16,-16l-32,0zM432,344l0,32c0,8.8 7.2,16 16,16l32,0c8.8,0 16,-7.2 16,-16l0,-32c0,-8.8 -7.2,-16 -16,-16l-32,0c-8.8,0 -16,7.2 -16,16zM800,328c-8.8,0 -16,7.2 -16,16l0,32c0,8.8 7.2,16 16,16l32,0c8.8,0 16,-7.2 16,-16l0,-32c0,-8.8 -7.2,-16 -16,-16l-32,0zM432,216l0,32c0,8.8 7.2,16 16,16l32,0c8.8,0 16,-7.2 16,-16l0,-32c0,-8.8 -7.2,-16 -16,-16L448,200c-8.8,0 -16,7.2 -16,16zM800,200c-8.8,0 -16,7.2 -16,16l0,32c0,8.8 7.2,16 16,16l32,0c8.8,0 16,-7.2 16,-16l0,-32c0,-8.8 -7.2,-16 -16,-16l-32,0zM544,232l0,64c0,17.7 14.3,32 32,32l128,0c17.7,0 32,-14.3 32,-32l0,-64c0,-17.7 -14.3,-32 -32,-32L576,200c-17.7,0 -32,14.3 -32,32zM576,392c-17.7,0 -32,14.3 -32,32l0,64c0,17.7 14.3,32 32,32l128,0c17.7,0 32,-14.3 32,-32l0,-64c0,-17.7 -14.3,-32 -32,-32l-128,0z"/> 4 | 5 | </vector> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/default_image.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="112.5dp" android:viewportHeight="720" android:viewportWidth="1280" android:width="200dp"> 2 | 3 | <path android:fillColor="#ffffff" android:pathData="M384,200C384,164.7 412.7,136 448,136L832,136c35.3,0 64,28.7 64,64L896,520c0,35.3 -28.7,64 -64,64L448,584c-35.3,0 -64,-28.7 -64,-64L384,200zM707.8,306.5c-4.5,-6.6 -11.9,-10.5 -19.8,-10.5s-15.4,3.9 -19.8,10.5l-87,127.6L554.7,401c-4.6,-5.7 -11.5,-9 -18.7,-9s-14.2,3.3 -18.7,9l-64,80c-5.8,7.2 -6.9,17.1 -2.9,25.4s12.4,13.6 21.6,13.6h96,32L808,520c8.9,0 17.1,-4.9 21.2,-12.8s3.6,-17.4 -1.4,-24.7l-120,-176zM496,296c26.5,0 48,-21.5 48,-48s-21.5,-48 -48,-48s-48,21.5 -48,48s21.5,48 48,48z"/> 4 | 5 | </vector> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/default_scene.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="112.5dp" android:viewportHeight="720" android:viewportWidth="1280" android:width="200dp"> 2 | 3 | <path android:fillColor="#ffffff" android:pathData="M896,360c0,141.4 -114.6,256 -256,256S384,501.4 384,360S498.6,104 640,104S896,218.6 896,360zM572.3,251.1c-7.6,4.2 -12.3,12.3 -12.3,20.9L560,448c0,8.7 4.7,16.7 12.3,20.9s16.8,4.1 24.3,-0.5l144,-88c7.1,-4.4 11.5,-12.1 11.5,-20.5s-4.4,-16.1 -11.5,-20.5l-144,-88c-7.4,-4.5 -16.7,-4.7 -24.3,-0.5z"/> 4 | 5 | </vector> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/default_studio.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="112.5dp" android:viewportHeight="720" android:viewportWidth="1280" android:width="200dp"> 2 | 3 | <path android:fillColor="#ffffff" android:pathData="M352,232C352,196.7 380.7,168 416,168L672,168c35.3,0 64,28.7 64,64L736,488c0,35.3 -28.7,64 -64,64L416,552c-35.3,0 -64,-28.7 -64,-64L352,232zM911.1,203.8c10.4,5.6 16.9,16.4 16.9,28.2L928,488c0,11.8 -6.5,22.6 -16.9,28.2s-23,5 -32.9,-1.6l-96,-64L768,441.1L768,424 768,296 768,278.9l14.2,-9.5 96,-64c9.8,-6.5 22.4,-7.2 32.9,-1.6z"/> 4 | 5 | </vector> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/default_tag.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="200dp" 3 | android:height="200dp" 4 | android:viewportWidth="200" 5 | android:viewportHeight="200"> 6 | <path 7 | android:pathData="m72.1,144.57 l-36.08,-36.08c-4.69,-4.69 -4.69,-12.28 0,-16.97l36.08,-36.08a12,12 0,0 1,8.49 -3.51l74.91,0c6.63,0 12,5.37 12,12l0,72.17c0,6.63 -5.37,12 -12,12l-74.91,0a12,12 0,0 1,-8.49 -3.51zM58.64,91.51c-4.69,4.69 -4.69,12.28 0,16.97 4.69,4.69 12.28,4.69 16.97,0 4.69,-4.69 4.69,-12.28 0,-16.97 -4.69,-4.69 -12.28,-4.69 -16.97,0z" 8 | android:fillColor="#ffffff"/> 9 | </vector> 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/favorite_button_selector.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true"> 4 | <ripple android:color="@color/selected_background"> 5 | <item> 6 | <shape android:shape="oval"> 7 | <solid android:color="@android:color/transparent"/> 8 | </shape> 9 | </item> 10 | </ripple> 11 | </item> 12 | <item android:state_focused="true"> 13 | <shape android:shape="oval"> 14 | <solid android:color="@color/selected_background" /> 15 | </shape> 16 | </item> 17 | <item> 18 | <shape android:shape="oval"> 19 | <solid android:color="@android:color/transparent" /> 20 | </shape> 21 | </item> 22 | </selector> 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/guided_actions_selector.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true"> 4 | <ripple android:color="@color/transparent_grey_25"/> 5 | </item> 6 | <item android:state_focused="true" android:drawable="@color/transparent_grey_25" /> 7 | <item android:drawable="@android:color/transparent"/> 8 | </selector> 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_button_selector.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true"> 4 | <layer-list> 5 | <item> 6 | <ripple android:color="@color/selected_background"> 7 | <item> 8 | <shape android:shape="oval"> 9 | <solid android:color="@color/selected_background"/> 10 | </shape> 11 | </item> 12 | </ripple> 13 | </item> 14 | <item android:drawable="@mipmap/stash_logo"/> 15 | </layer-list> 16 | </item> 17 | <item android:state_focused="true"> 18 | <layer-list> 19 | <item> 20 | <shape android:shape="oval"> 21 | <solid android:color="@color/selected_background"/> 22 | </shape> 23 | </item> 24 | <item android:drawable="@mipmap/stash_logo"/> 25 | </layer-list> 26 | </item> 27 | <item android:drawable="@mipmap/stash_logo"/> 28 | </selector> 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/image_button_selector.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true"> 4 | <ripple android:color="@color/selected_background"/> 5 | </item> 6 | <item android:state_focused="true" android:drawable="@color/selected_background" /> 7 | <item android:drawable="@android:color/transparent" /> 8 | </selector> 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/playback_button_selector.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true"> 4 | <ripple android:color="@android:color/black"> 5 | <item> 6 | <shape android:shape="oval"> 7 | <solid android:color="@android:color/transparent"/> 8 | </shape> 9 | </item> 10 | </ripple> 11 | </item> 12 | <item android:state_focused="true"> 13 | <shape android:shape="oval"> 14 | <solid android:color="@color/selected_background"/> 15 | </shape> 16 | </item> 17 | <item> 18 | <shape android:shape="oval"> 19 | <solid android:color="@color/transparent_black_25" /> 20 | </shape> 21 | </item> 22 | </selector> 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/playback_popup_item_background.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true"> 4 | <ripple android:color="@color/popup_selected_background"/> 5 | </item> 6 | <item android:state_focused="true" android:color="@color/popup_selected_background"> 7 | <shape android:shape="rectangle"> 8 | <solid android:color="@color/popup_selected_background" /> 9 | </shape> 10 | </item> 11 | <item android:color="@color/popup_background"> 12 | <shape android:shape="rectangle"> 13 | <solid android:color="@color/popup_background" /> 14 | </shape> 15 | </item> 16 | </selector> 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/popup_item_background.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true"> 4 | <ripple android:color="@color/popup_selected_background"/> 5 | </item> 6 | <item android:state_selected="true" android:color="@color/popup_selected_background"> 7 | <shape android:shape="rectangle"> 8 | <solid android:color="@color/popup_selected_background" /> 9 | </shape> 10 | </item> 11 | <item android:color="@color/popup_background"> 12 | <shape android:shape="rectangle"> 13 | <solid android:color="@color/popup_background" /> 14 | </shape> 15 | </item> 16 | </selector> 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rectangle.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <shape xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:shape="rectangle"> 4 | <solid android:color="@android:color/white"/> 5 | </shape> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/scrollbar_thumb.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_focused="true" android:color="@color/selected_background"> 4 | <shape android:shape="rectangle"> 5 | <solid android:color="@color/selected_background" /> 6 | </shape> 7 | </item> 8 | <item android:color="@android:color/darker_gray"> 9 | <shape android:shape="rectangle"> 10 | <solid android:color="@android:color/darker_gray" /> 11 | </shape> 12 | </item> 13 | </selector> 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/search_cursor.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <shape xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <size android:width="3dp" /> 4 | <solid android:color="@color/selected_background" /> 5 | </shape> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selected_rectangle.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <shape xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:shape="rectangle"> 4 | <solid android:color="@color/selected_background"/> 5 | </shape> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sweat_drops.xml: -------------------------------------------------------------------------------- 1 | <!-- Modified from https://github.com/stashapp/stash/blob/v0.24.3/ui/v2.5/src/components/Shared/SweatDrops.tsx --> 2 | <vector android:height="150dp" android:viewportHeight="36" 3 | android:viewportWidth="36" android:width="150dp" xmlns:android="http://schemas.android.com/apk/res/android"> 4 | <path android:fillColor="#ffffff" android:pathData="M22.855,0.758L7.875,7.024l12.537,9.733c2.633,2.224 6.377,2.937 9.77,1.518c4.826,-2.018 7.096,-7.576 5.072,-12.413C33.232,1.024 27.68,-1.261 22.855,0.758zM12.893,18.682L2.05,10.284L0.137,23.529a7.993,7.993 0,0 0,2.958 7.803a8.001,8.001 0,0 0,9.798 -12.65zM28.232,25.697l-8.156,-4.69l-0.033,9.223c-0.088,2 0.904,3.98 2.75,5.041a5.462,5.462 0,0 0,7.479 -2.051c1.499,-2.644 0.589,-6.013 -2.04,-7.523z"/> 5 | <path android:fillColor="#00000000" android:pathData="M0,0h36v36h-36z"/> 6 | </vector> 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/transparent_button_selector.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <selector xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <item android:state_pressed="true"> 4 | <ripple android:color="@color/selected_background"/> 5 | </item> 6 | <item android:state_focused="true" android:drawable="@color/selected_background" /> 7 | <item android:drawable="@android:color/transparent" /> 8 | </selector> 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/vector_settings.xml: -------------------------------------------------------------------------------- 1 | <vector android:height="32dp" android:tint="#FFFFFF" 2 | android:viewportHeight="24" android:viewportWidth="24" 3 | android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android"> 4 | <path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/> 5 | </vector> 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_frame.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <shape xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:shape="rectangle"> 4 | 5 | <stroke 6 | android:width="2dp" 7 | android:color="@android:color/white" /> 8 | 9 | <solid android:color="@android:color/black" /> 10 | </shape> 11 | -------------------------------------------------------------------------------- /app/src/main/res/font/fa_solid_900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damontecres/StashAppAndroidTV/4fae183567d1809898659abb346a000c26b46e65/app/src/main/res/font/fa_solid_900.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_root.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools" 4 | android:id="@+id/root_fragment" 5 | android:layout_width="match_parent" 6 | android:layout_height="match_parent" 7 | tools:deviceIds="tv" 8 | tools:ignore="MergeRootFrame" 9 | android:theme="@style/Widget.Theme.StashAppAndroidTV.MyView"> 10 | 11 | <ImageView 12 | android:id="@+id/background_logo" 13 | android:layout_width="@dimen/title_bar_icon_size" 14 | android:layout_height="@dimen/title_bar_icon_size" 15 | android:layout_margin="@dimen/title_bar_margin" 16 | android:src="@mipmap/stash_logo" 17 | android:contentDescription="@string/app_name" /> 18 | 19 | <androidx.core.widget.ContentLoadingProgressBar 20 | android:id="@+id/loading_progress_bar" 21 | android:layout_width="wrap_content" 22 | android:layout_height="wrap_content" 23 | android:layout_gravity="center" 24 | android:visibility="visible" 25 | android:background="@android:color/transparent" 26 | style="?android:attr/progressBarStyleLarge" /> 27 | </FrameLayout> 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_root_compose.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools" 4 | android:layout_width="match_parent" 5 | android:layout_height="match_parent" 6 | tools:deviceIds="tv" 7 | tools:ignore="MergeRootFrame" 8 | android:theme="@style/Widget.Theme.StashAppAndroidTV.MyView"> 9 | 10 | <!-- <androidx.fragment.app.FragmentContainerView--> 11 | <!-- android:id="@+id/nav_drawer_fragment"--> 12 | <!-- android:layout_width="match_parent"--> 13 | <!-- android:layout_height="match_parent"--> 14 | <!-- android:name="com.github.damontecres.stashapp.ui.NavDrawerFragment" />--> 15 | 16 | <FrameLayout 17 | android:id="@+id/root_fragment" 18 | android:layout_width="match_parent" 19 | android:layout_height="match_parent" 20 | android:visibility="visible"> 21 | 22 | 23 | <ImageView 24 | android:id="@+id/background_logo" 25 | android:layout_width="@dimen/title_bar_icon_size" 26 | android:layout_height="@dimen/title_bar_icon_size" 27 | android:layout_margin="@dimen/title_bar_margin" 28 | android:src="@mipmap/stash_logo" 29 | android:contentDescription="@string/app_name" /> 30 | 31 | <androidx.core.widget.ContentLoadingProgressBar 32 | android:id="@+id/loading_progress_bar" 33 | android:layout_width="wrap_content" 34 | android:layout_height="wrap_content" 35 | android:layout_gravity="center" 36 | android:visibility="visible" 37 | android:background="@android:color/transparent" 38 | style="?android:attr/progressBarStyleLarge" /> 39 | </FrameLayout> 40 | </FrameLayout> 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_root_pin.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools" 4 | android:id="@+id/root_activity_fragment" 5 | android:layout_width="match_parent" 6 | android:layout_height="match_parent" 7 | tools:deviceIds="tv" 8 | tools:ignore="MergeRootFrame" 9 | android:theme="@style/Widget.Theme.StashAppAndroidTV.MyView"> 10 | 11 | <androidx.fragment.app.FragmentContainerView 12 | android:id="@+id/pin_fragment_view" 13 | android:layout_width="match_parent" 14 | android:layout_height="match_parent" 15 | android:name="com.github.damontecres.stashapp.PinFragmentCompose"/> 16 | </FrameLayout> 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/alphabet_button.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <Button xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools" 4 | android:layout_width="20dp" 5 | android:layout_height="20dp" 6 | android:layout_weight="1" 7 | android:layout_margin="2dp" 8 | android:background="@drawable/button_selector" 9 | android:textSize="12sp" 10 | android:textColor="@android:color/white" 11 | tools:text="A"> 12 | </Button> 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/changelog.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:layout_width="match_parent" 4 | android:layout_height="match_parent" 5 | android:fadeScrollbars="false" 6 | android:scrollbarThumbVertical="@drawable/selected_rectangle"> 7 | <TextView 8 | android:id="@+id/changelog_text" 9 | android:layout_width="match_parent" 10 | android:layout_height="wrap_content" 11 | android:padding="20dp" 12 | android:textSize="18sp" 13 | /> 14 | </ScrollView> 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/compose_frame.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:layout_width="match_parent" 4 | android:layout_height="match_parent"> 5 | 6 | <androidx.compose.ui.platform.ComposeView 7 | android:id="@+id/compose_view" 8 | android:layout_width="match_parent" 9 | android:layout_height="match_parent" /> 10 | 11 | </FrameLayout> 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/debug_supported_row.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <TableRow xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools" 4 | android:layout_width="match_parent" 5 | android:layout_height="match_parent"> 6 | 7 | <TextView 8 | style="@style/DebugInfoOverlayText" 9 | tools:text="@string/id" /> 10 | 11 | <TextView 12 | style="@style/DebugInfoOverlayText" 13 | tools:text="@string/stashapp_type" /> 14 | 15 | <TextView 16 | style="@style/DebugInfoOverlayText" 17 | tools:text="@string/selected" /> 18 | 19 | <TextView 20 | style="@style/DebugInfoOverlayText" 21 | tools:text="@string/codec" /> 22 | 23 | <TextView 24 | style="@style/DebugInfoOverlayText" 25 | tools:text="@string/supported" /> 26 | 27 | <TextView 28 | style="@style/DebugInfoOverlayText" 29 | tools:text="@string/labels" /> 30 | 31 | </TableRow> 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/duration_guided_action.xml: -------------------------------------------------------------------------------- 1 | <androidx.leanback.widget.GuidedActionItemContainer xmlns:android="http://schemas.android.com/apk/res/android" 2 | xmlns:tools="http://schemas.android.com/tools" 3 | tools:viewBindingIgnore="true" 4 | style="?attr/guidedActionItemContainerStyle"> 5 | 6 | <ImageView 7 | android:id="@+id/guidedactions_item_icon" 8 | style="?attr/guidedActionItemIconStyle" 9 | tools:ignore="ContentDescription" /> 10 | 11 | <TextView 12 | android:id="@+id/guidedactions_item_title" 13 | android:layout_width="wrap_content" 14 | android:layout_gravity="center_vertical" 15 | style="?attr/guidedActionItemTitleStyle" /> 16 | 17 | <com.github.damontecres.stashapp.views.DurationPicker 18 | android:id="@+id/guidedactions_activator_item" 19 | android:importantForAccessibility="yes" 20 | android:layout_width="wrap_content" 21 | android:layout_height="wrap_content" /> 22 | </androidx.leanback.widget.GuidedActionItemContainer> 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/filter_debug_page.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:orientation="vertical" 4 | android:layout_width="match_parent" 5 | android:layout_height="match_parent"> 6 | <TextView 7 | android:id="@+id/page_number_text" 8 | android:layout_width="wrap_content" 9 | android:layout_height="wrap_content" 10 | android:text="Page" 11 | android:textSize="24sp"/> 12 | <TableLayout 13 | android:id="@+id/page_table" 14 | android:layout_width="match_parent" 15 | android:layout_height="wrap_content"> 16 | <TableRow> 17 | <TextView 18 | android:layout_width="wrap_content" 19 | android:layout_height="wrap_content" 20 | android:layout_margin="4dp" 21 | android:text="Position" 22 | android:textSize="20sp"/> 23 | <TextView 24 | android:layout_width="wrap_content" 25 | android:layout_height="wrap_content" 26 | android:layout_margin="4dp" 27 | android:text="ID" 28 | android:textSize="20sp"/> 29 | <TextView 30 | android:layout_width="wrap_content" 31 | android:layout_height="wrap_content" 32 | android:layout_margin="4dp" 33 | android:text="Title" 34 | android:textSize="20sp"/> 35 | </TableRow> 36 | </TableLayout> 37 | 38 | </LinearLayout> 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/filter_debug_page_row.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <TableRow xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:layout_width="match_parent" 4 | android:layout_height="match_parent"> 5 | <TextView 6 | android:layout_width="wrap_content" 7 | android:layout_height="wrap_content" 8 | android:layout_margin="4dp" 9 | android:text="Position" 10 | android:textSize="16sp"/> 11 | <TextView 12 | android:layout_width="wrap_content" 13 | android:layout_height="wrap_content" 14 | android:layout_margin="4dp" 15 | android:text="ID" 16 | android:textSize="16sp"/> 17 | <TextView 18 | android:layout_width="wrap_content" 19 | android:layout_height="wrap_content" 20 | android:layout_margin="4dp" 21 | android:text="Title" 22 | android:textSize="16sp"/> 23 | 24 | </TableRow> 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/frame.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:id="@+id/frame_content" 4 | android:layout_width="match_parent" 5 | android:layout_height="match_parent" /> 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/grid_footer_layout.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools" 4 | android:orientation="horizontal" 5 | android:layout_width="wrap_content" 6 | android:layout_height="wrap_content" 7 | android:background="@color/transparent_black_75"> 8 | 9 | <TextView 10 | android:id="@+id/position_text" 11 | style="@style/GridFooterText" 12 | android:text="\?" 13 | tools:text="11" 14 | tools:ignore="HardcodedText" /> 15 | 16 | <TextView 17 | style="@style/GridFooterText" 18 | android:text="/" 19 | tools:ignore="HardcodedText" /> 20 | 21 | <TextView 22 | android:id="@+id/total_count_text" 23 | style="@style/GridFooterText" 24 | android:text="\?" 25 | tools:text="100" 26 | tools:ignore="HardcodedText" /> 27 | </LinearLayout> 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/image_action_button.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <Button xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools" 4 | android:id="@+id/action_button" 5 | android:layout_width="wrap_content" 6 | android:layout_height="wrap_content" 7 | android:layout_margin="4dp" 8 | android:padding="4dp" 9 | android:minWidth="0dp" 10 | android:minHeight="0dp" 11 | android:lines="1" 12 | android:fontFamily="@font/fa_solid_900" 13 | android:textSize="32sp" 14 | android:background="@drawable/image_button_selector" 15 | tools:text="@string/fa_rotate_left" 16 | /> 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/image_card_extra_content_row.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:app="http://schemas.android.com/apk/res-auto" 4 | xmlns:tools="http://schemas.android.com/tools" 5 | android:id="@+id/card_extra_content_row" 6 | android:visibility="gone" 7 | android:layout_width="match_parent" 8 | android:layout_height="18dp" 9 | android:layout_alignParentStart="true" 10 | android:layout_below="@+id/content_text" 11 | android:layout_marginStart="0dp" 12 | android:layout_marginEnd="0dp" 13 | android:layout_gravity="center_horizontal" 14 | android:gravity="center_horizontal" 15 | tools:visibility="visible"> 16 | 17 | <TextView 18 | android:id="@+id/extra_content_text" 19 | app:layout_constraintStart_toStartOf="parent" 20 | app:layout_constraintEnd_toEndOf="parent" 21 | app:layout_constraintTop_toTopOf="parent" 22 | app:layout_constraintBottom_toBottomOf="parent" 23 | app:layout_constraintHorizontal_chainStyle="packed" 24 | style="@style/Widget.Leanback.ImageCardView.ContentStyle" 25 | android:singleLine="true" 26 | android:marqueeRepeatLimit="-1" 27 | android:ellipsize="marquee" 28 | tools:text="Some content text" /> 29 | </androidx.constraintlayout.widget.ConstraintLayout> 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/image_card_icon_row.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:app="http://schemas.android.com/apk/res-auto" 4 | xmlns:tools="http://schemas.android.com/tools" 5 | android:id="@+id/card_icon_row" 6 | android:layout_width="match_parent" 7 | android:layout_height="18dp" 8 | android:layout_alignParentStart="true" 9 | android:layout_below="@+id/card_extra_content_row" 10 | android:layout_marginStart="0dp" 11 | android:layout_marginEnd="0dp" 12 | android:layout_gravity="center_horizontal" 13 | android:gravity="center_horizontal"> 14 | 15 | <TextView 16 | android:id="@+id/icon_text" 17 | android:visibility="visible" 18 | app:layout_constraintStart_toStartOf="parent" 19 | app:layout_constraintEnd_toEndOf="parent" 20 | app:layout_constraintTop_toTopOf="parent" 21 | app:layout_constraintBottom_toBottomOf="parent" 22 | app:layout_constraintHorizontal_chainStyle="packed" 23 | style="@style/Widget.Theme.StashAppAndroidTV.SceneExtraText" 24 | android:singleLine="true" 25 | android:marqueeRepeatLimit="-1" 26 | android:ellipsize="marquee" 27 | tools:text="1" /> 28 | </androidx.constraintlayout.widget.ConstraintLayout> 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/image_clip_playback.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:app="http://schemas.android.com/apk/res-auto" 4 | android:id="@+id/video_playback_frame" 5 | android:layout_width="match_parent" 6 | android:layout_height="match_parent" 7 | android:background="@android:color/black"> 8 | 9 | <com.github.damontecres.stashapp.playback.StashPlayerView 10 | android:id="@+id/video_view" 11 | android:layout_width="match_parent" 12 | android:layout_height="match_parent" 13 | app:show_buffering="when_playing" 14 | app:auto_show="false" 15 | app:hide_on_touch="true" 16 | app:animation_enabled="true" 17 | app:scrubber_dragged_size="22dp" 18 | app:played_color="@color/selected_background" 19 | app:use_controller="true" 20 | app:hide_during_ads="false" 21 | app:show_subtitle_button="true" /> 22 | 23 | </FrameLayout> 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/image_fragment.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools" 4 | android:id="@+id/root" 5 | android:layout_width="match_parent" 6 | android:layout_height="match_parent" 7 | tools:deviceIds="tv" 8 | tools:ignore="MergeRootFrame"> 9 | 10 | </FrameLayout> 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/image_layout.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools" 4 | xmlns:app="http://schemas.android.com/apk/res-auto" 5 | android:layout_width="match_parent" 6 | android:layout_height="match_parent" 7 | android:background="@android:color/black" 8 | tools:background="@android:color/darker_gray"> 9 | 10 | <com.github.damontecres.stashapp.views.StashZoomImageView 11 | android:id="@+id/image_view_image" 12 | android:layout_width="match_parent" 13 | android:layout_height="match_parent" 14 | android:scrollbars="vertical|horizontal" 15 | android:background="@android:color/black" 16 | app:transformation="centerInside" 17 | app:transformationGravity="auto" 18 | app:alignment="center" 19 | app:overScrollHorizontal="true" 20 | app:overScrollVertical="true" 21 | app:overPinchable="false" 22 | app:horizontalPanEnabled="true" 23 | app:verticalPanEnabled="true" 24 | app:zoomEnabled="true" 25 | app:flingEnabled="false" 26 | app:scrollEnabled="true" 27 | app:oneFingerScrollEnabled="true" 28 | app:twoFingersScrollEnabled="true" 29 | app:threeFingersScrollEnabled="true" 30 | app:minZoom="1.0" 31 | app:minZoomType="zoom" 32 | app:maxZoom="5.0" 33 | app:maxZoomType="zoom" 34 | app:animationDuration="250" /> 35 | 36 | </FrameLayout> 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/jump_buttons.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:layout_width="wrap_content" 4 | android:layout_height="wrap_content" 5 | android:orientation="vertical" 6 | android:visibility="visible"> 7 | 8 | <Button 9 | android:id="@+id/jump_up_2_button" 10 | android:layout_width="@dimen/jump_button" 11 | android:layout_height="@dimen/jump_button" 12 | style="@style/JumpButton" 13 | android:text="@string/fa_angles_up" 14 | android:nextFocusDown="@id/jump_up_1_button"> 15 | </Button> 16 | <Button 17 | android:id="@+id/jump_up_1_button" 18 | android:layout_width="@dimen/jump_button" 19 | android:layout_height="@dimen/jump_button" 20 | style="@style/JumpButton" 21 | android:text="@string/fa_angle_up" 22 | android:nextFocusUp="@id/jump_up_2_button" 23 | android:nextFocusDown="@id/jump_down_1_button"> 24 | </Button> 25 | <Button 26 | android:id="@+id/jump_down_1_button" 27 | android:layout_width="@dimen/jump_button" 28 | android:layout_height="@dimen/jump_button" 29 | style="@style/JumpButton" 30 | android:text="@string/fa_angle_down" 31 | android:nextFocusUp="@id/jump_up_1_button" 32 | android:nextFocusDown="@id/jump_down_2_button"> 33 | </Button> 34 | <Button 35 | android:id="@+id/jump_down_2_button" 36 | android:layout_width="@dimen/jump_button" 37 | android:layout_height="@dimen/jump_button" 38 | style="@style/JumpButton" 39 | android:text="@string/fa_angles_down" 40 | android:nextFocusUp="@id/jump_down_1_button"> 41 | </Button> 42 | 43 | 44 | </LinearLayout> 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/lb_details_description.xml: -------------------------------------------------------------------------------- 1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 | xmlns:app="http://schemas.android.com/apk/res-auto" 3 | android:orientation="vertical" 4 | android:layout_width="match_parent" 5 | android:layout_height="wrap_content"> 6 | 7 | <TextView 8 | android:id="@+id/lb_details_description_title" 9 | android:layout_width="wrap_content" 10 | android:layout_height="wrap_content" 11 | style="?attr/detailsDescriptionTitleStyle" /> 12 | 13 | <com.github.damontecres.stashapp.views.StashRatingBar 14 | android:id="@+id/rating_bar" 15 | android:layout_width="300dp" 16 | android:layout_height="wrap_content" 17 | app:defaultRating100="0" 18 | android:textSize="16sp" 19 | android:textColor="@android:color/darker_gray" 20 | android:background="@drawable/transparent_button_selector" /> 21 | 22 | <TextView 23 | android:id="@+id/lb_details_description_subtitle" 24 | android:layout_width="wrap_content" 25 | android:layout_height="wrap_content" 26 | style="?attr/detailsDescriptionSubtitleStyle" /> 27 | 28 | <androidx.core.widget.NestedScrollView 29 | android:id="@+id/description_scrollview" 30 | android:layout_width="match_parent" 31 | android:layout_height="wrap_content" 32 | android:paddingBottom="25dp" 33 | android:fadeScrollbars="false" 34 | android:scrollbars="vertical" 35 | android:scrollbarAlwaysDrawVerticalTrack="true" 36 | android:scrollbarThumbVertical="@drawable/scrollbar_thumb"> 37 | 38 | <TextView 39 | android:id="@+id/lb_details_description_body" 40 | android:layout_width="wrap_content" 41 | android:layout_height="wrap_content" 42 | android:paddingStart="0dp" 43 | android:paddingEnd="8dp" 44 | style="?attr/detailsDescriptionBodyStyle" /> 45 | </androidx.core.widget.NestedScrollView> 46 | </LinearLayout> 47 | -------------------------------------------------------------------------------- /app/src/main/res/layout/license.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:layout_width="match_parent" 4 | android:layout_height="match_parent"> 5 | 6 | <TextView 7 | android:id="@+id/license_text" 8 | android:layout_width="match_parent" 9 | android:layout_height="wrap_content" 10 | android:textAlignment="center"/> 11 | </ScrollView> 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/loading_fragment.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:layout_width="match_parent" 4 | android:layout_height="match_parent"> 5 | <androidx.core.widget.ContentLoadingProgressBar 6 | android:id="@+id/loading_progress_bar" 7 | android:layout_width="wrap_content" 8 | android:layout_height="wrap_content" 9 | android:layout_gravity="center" 10 | android:visibility="visible" 11 | style="?android:attr/progressBarStyleLarge" /> 12 | </FrameLayout> 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main_title_view.xml: -------------------------------------------------------------------------------- 1 | <com.github.damontecres.stashapp.views.MainTitleView xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:id="@+id/browse_title_group" 3 | android:layout_width="match_parent" 4 | android:layout_height="wrap_content"> 5 | 6 | </com.github.damontecres.stashapp.views.MainTitleView> 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/performer_details_compose.xml: -------------------------------------------------------------------------------- 1 | <androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:id="@+id/fragment_container_view" 3 | android:layout_width="match_parent" 4 | android:layout_height="match_parent" 5 | android:name="com.github.damontecres.stashapp.PerformerDetailsFragment" /> 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/pin_dialog.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:orientation="vertical" 4 | android:layout_width="match_parent" 5 | android:layout_height="match_parent"> 6 | 7 | <TextView 8 | android:layout_width="match_parent" 9 | android:layout_height="80dp" 10 | android:text="@string/enter_pin" 11 | android:textSize="50sp" 12 | android:textAlignment="center" 13 | android:labelFor="@id/pin_edit_text" /> 14 | 15 | <EditText 16 | android:id="@+id/pin_edit_text" 17 | android:inputType="numberPassword" 18 | android:layout_width="match_parent" 19 | android:layout_height="80dp" 20 | android:autofillHints="password" /> 21 | 22 | <Button 23 | android:id="@+id/pin_submit" 24 | android:layout_width="wrap_content" 25 | android:layout_height="wrap_content" 26 | android:layout_gravity="end" 27 | android:text="@string/stashapp_actions_submit" /> 28 | 29 | </LinearLayout> 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/playlist_list.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools" 4 | android:layout_width="wrap_content" 5 | android:layout_height="match_parent" 6 | android:orientation="vertical" 7 | android:background="@color/default_background" 8 | tools:layout_width="300dp"> 9 | 10 | <TextView 11 | android:id="@+id/playlist_title" 12 | android:layout_width="match_parent" 13 | android:layout_height="wrap_content" 14 | android:layout_margin="4dp" 15 | android:padding="4dp" 16 | android:textSize="28sp" 17 | android:textAlignment="center" 18 | android:maxLines="1" 19 | android:ellipsize="end" 20 | tools:text="Playlist Title" /> 21 | 22 | <FrameLayout 23 | android:id="@+id/list_view" 24 | android:layout_width="match_parent" 25 | android:layout_height="match_parent" /> 26 | </LinearLayout> 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/popup_header.xml: -------------------------------------------------------------------------------- 1 | <TextView xmlns:android="http://schemas.android.com/apk/res/android" 2 | xmlns:tools="http://schemas.android.com/tools" 3 | android:id="@+id/popup_item_text" 4 | android:layout_width="match_parent" 5 | android:layout_height="wrap_content" 6 | android:textSize="14sp" 7 | android:background="@drawable/popup_item_background" 8 | android:gravity="center" 9 | android:paddingStart="?android:attr/listPreferredItemPaddingStart" 10 | android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" 11 | android:minHeight="32dp" 12 | android:singleLine="true" 13 | android:maxLines="1" 14 | android:ellipsize="marquee" 15 | android:marqueeRepeatLimit="marquee_forever" 16 | tools:text="Header" 17 | /> 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/popup_item.xml: -------------------------------------------------------------------------------- 1 | <TextView xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:id="@+id/popup_item_text" 3 | android:layout_width="match_parent" 4 | android:layout_height="wrap_content" 5 | android:textAppearance="?android:attr/textAppearanceListItemSmall" 6 | android:background="@drawable/popup_item_background" 7 | android:gravity="center_vertical" 8 | android:paddingStart="?android:attr/listPreferredItemPaddingStart" 9 | android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" 10 | android:minHeight="?android:attr/listPreferredItemHeightSmall" 11 | android:singleLine="true" 12 | android:maxLines="1" 13 | android:ellipsize="marquee" 14 | android:marqueeRepeatLimit="marquee_forever" /> 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/root_fragment_layout.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:id="@+id/root_fragment" 4 | android:layout_width="match_parent" 5 | android:layout_height="match_parent"> 6 | 7 | </FrameLayout> 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/skip_indicator.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:layout_width="match_parent" 4 | android:layout_height="match_parent" 5 | tools:layout_width="55dp" 6 | tools:layout_height="55dp" 7 | xmlns:tools="http://schemas.android.com/tools"> 8 | 9 | <ImageView 10 | android:id="@+id/duration_image" 11 | android:layout_width="match_parent" 12 | android:layout_height="match_parent" 13 | android:contentDescription="@null" 14 | android:padding="6dp" 15 | tools:rotation="40" 16 | tools:src="@drawable/circular_arrow_right" /> 17 | 18 | <TextView 19 | android:id="@+id/duration_text" 20 | android:layout_width="match_parent" 21 | android:layout_height="match_parent" 22 | android:gravity="center" 23 | android:textAlignment="center" 24 | android:textColor="@android:color/white" 25 | android:textSize="13sp" 26 | android:textStyle="bold" 27 | tools:text="3000" /> 28 | 29 | </FrameLayout> 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/sort_popup_item.xml: -------------------------------------------------------------------------------- 1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:layout_width="match_parent" 3 | android:layout_height="wrap_content" 4 | xmlns:tools="http://schemas.android.com/tools" 5 | android:background="@drawable/popup_item_background" 6 | android:orientation="horizontal"> 7 | <TextView 8 | android:id="@+id/sort_indicator" 9 | android:layout_width="32dp" 10 | android:layout_height="wrap_content" 11 | android:minWidth="32dp" 12 | android:textAppearance="?android:attr/textAppearanceListItemSmall" 13 | android:background="@drawable/popup_item_background" 14 | android:gravity="center_vertical" 15 | android:paddingStart="?android:attr/listPreferredItemPaddingStart" 16 | android:paddingEnd="4dp" 17 | android:minHeight="?android:attr/listPreferredItemHeightSmall" 18 | android:singleLine="true" 19 | android:maxLines="1" 20 | android:text="@null" 21 | android:fontFamily="@font/fa_solid_900" 22 | tools:text="@string/fa_caret_down"/> 23 | <TextView 24 | android:id="@+id/popup_item_text" 25 | android:layout_width="match_parent" 26 | android:layout_height="wrap_content" 27 | android:textAppearance="?android:attr/textAppearanceListItemSmall" 28 | android:background="@drawable/popup_item_background" 29 | android:gravity="center_vertical" 30 | android:paddingStart="4dp" 31 | android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" 32 | android:minHeight="?android:attr/listPreferredItemHeightSmall" 33 | android:singleLine="true" 34 | android:maxLines="1" 35 | android:ellipsize="marquee" 36 | android:marqueeRepeatLimit="marquee_forever" 37 | tools:text="Filename"/> 38 | </LinearLayout> 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/stash_card_player_view.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <androidx.media3.ui.PlayerView xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:app="http://schemas.android.com/apk/res-auto" 4 | android:id="@+id/main_video" 5 | android:visibility="gone" 6 | android:layout_width="match_parent" 7 | android:layout_height="match_parent" 8 | android:adjustViewBounds="true" 9 | android:background="@android:color/black" 10 | app:use_controller="false" 11 | app:show_buffering="never" 12 | app:auto_show="false" /> 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/stash_rating_bar.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <merge xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:layout_width="match_parent" 4 | android:layout_height="match_parent" 5 | xmlns:tools="http://schemas.android.com/tools"> 6 | 7 | <com.github.damontecres.stashapp.views.StarRatingBar 8 | android:id="@+id/rating_star" 9 | android:layout_width="wrap_content" 10 | android:layout_height="match_parent" 11 | android:numStars="5" 12 | android:stepSize=".5" 13 | android:isIndicator="false" 14 | android:gravity="center" 15 | android:theme="@style/RatingBar" 16 | /> 17 | 18 | <LinearLayout 19 | android:id="@+id/rating_decimal_holder" 20 | android:layout_width="wrap_content" 21 | android:layout_height="match_parent" 22 | android:orientation="horizontal"> 23 | <TextView 24 | android:id="@+id/rating_decimal_text" 25 | android:layout_width="wrap_content" 26 | android:layout_height="match_parent" 27 | android:gravity="center" 28 | android:textAlignment="center" 29 | tools:text="@string/stashapp_rating" /> 30 | 31 | <com.github.damontecres.stashapp.views.WrapAroundSeekBar 32 | android:id="@+id/rating_decimal" 33 | android:layout_width="match_parent" 34 | android:minWidth="160dp" 35 | android:layout_height="match_parent" 36 | android:layout_marginStart="10dp" 37 | android:gravity="center" 38 | android:max="100" /> 39 | </LinearLayout> 40 | 41 | </merge> 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/table_row.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <TableRow xmlns:android="http://schemas.android.com/apk/res/android" 3 | android:id="@+id/table_row" 4 | android:layout_width="match_parent" 5 | android:layout_height="wrap_content" 6 | android:layout_gravity="center_horizontal"> 7 | 8 | <TextView 9 | android:id="@+id/table_row_key" 10 | android:layout_width="wrap_content" 11 | android:layout_height="wrap_content" 12 | 13 | android:maxLines="1" 14 | android:padding="2dp" 15 | android:textAlignment="viewStart" 16 | android:textColor="@android:color/white" 17 | android:textSize="@dimen/table_text_size" 18 | 19 | /> 20 | 21 | <TextView 22 | android:id="@+id/table_row_value" 23 | android:layout_width="wrap_content" 24 | android:layout_height="wrap_content" 25 | 26 | android:padding="2dp" 27 | android:textAlignment="viewStart" 28 | android:textColor="@android:color/white" 29 | android:textSize="@dimen/table_text_size" 30 | /> 31 | 32 | </TableRow> 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/video_playback.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:app="http://schemas.android.com/apk/res-auto" 4 | xmlns:tools="http://schemas.android.com/tools" 5 | android:id="@+id/video_playback_frame" 6 | android:layout_width="match_parent" 7 | android:layout_height="match_parent" 8 | android:background="@android:color/black"> 9 | 10 | <com.github.damontecres.stashapp.playback.StashPlayerView 11 | android:id="@+id/video_view" 12 | android:layout_width="match_parent" 13 | android:layout_height="match_parent" 14 | app:show_buffering="when_playing" 15 | app:auto_show="false" 16 | app:hide_on_touch="true" 17 | app:animation_enabled="true" 18 | app:scrubber_dragged_size="22dp" 19 | app:played_color="@color/selected_background" 20 | app:use_controller="true" 21 | app:hide_during_ads="false" 22 | app:show_subtitle_button="true" /> 23 | 24 | <FrameLayout 25 | android:id="@+id/video_overlay" 26 | android:layout_width="match_parent" 27 | android:layout_height="match_parent" /> 28 | 29 | <com.github.damontecres.stashapp.views.SkipIndicator 30 | android:id="@+id/skip_indicator" 31 | android:layout_width="65dp" 32 | android:layout_height="65dp" 33 | android:layout_gravity="bottom|center" 34 | android:layout_marginBottom="70dp" 35 | android:visibility="gone" 36 | tools:visibility="visible" 37 | tools:background="@color/transparent_black_50" /> 38 | 39 | </FrameLayout> 40 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-nodpi/stash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damontecres/StashAppAndroidTV/4fae183567d1809898659abb346a000c26b46e65/app/src/main/res/mipmap-nodpi/stash_logo.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | <resources> 2 | 3 | <style name="Widget.Theme.StashAppAndroidTV.MyView" parent=""> 4 | <item name="android:background">@color/default_background</item> 5 | </style> 6 | </resources> 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs_main_title_view.xml: -------------------------------------------------------------------------------- 1 | <resources> 2 | <declare-styleable name="MainTitleView"> 3 | <attr name="exampleString" format="string" /> 4 | <attr name="exampleDimension" format="dimension" /> 5 | <attr name="exampleColor" format="color" /> 6 | <attr name="exampleDrawable" format="color|reference" /> 7 | </declare-styleable> 8 | </resources> 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs_rating_bar.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <resources> 3 | <declare-styleable name="StashRatingBar"> 4 | <attr name="android:textColor"/> 5 | <attr name="android:textSize"/> 6 | <attr name="defaultRating100" format="integer" /> 7 | <attr name="android:background" /> 8 | </declare-styleable> 9 | </resources> 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | <resources> 2 | <color name="fastlane_background">#0096a6</color> 3 | <color name="selected_background">#4785b5</color> 4 | <color name="default_background">#202b33</color> 5 | <color name="default_card_background">#30404d</color> 6 | <color name="gold">#FFD700</color> 7 | <color name="popup_selected_background">#3c719a</color> 8 | <color name="popup_background">#111a20</color> 9 | <!-- Transparent levels https://gist.github.com/lopspower/03fb1cc0ac9f32ef38f4 --> 10 | <color name="transparent_black_25">#40000000</color> 11 | <color name="transparent_black_50">#80000000</color> 12 | <color name="transparent_black_75">#BF000000</color> 13 | <color name="transparent_grey_25">#40888888</color> 14 | <color name="seek_bar">#5db2e0</color> 15 | <color name="transparent_red_50">#80FF0000</color> 16 | 17 | <color name="transparent_default_card_background_25">#4030404d</color> 18 | <color name="transparent_default_card_background_50">#8030404d</color> 19 | <color name="transparent_default_card_background_75">#BF30404d</color> 20 | <color name="selected_background_50">#804785b5</color> 21 | 22 | <array name="rating_colors"> 23 | <item>#80939393</item> 24 | <item>#809b8c7d</item> 25 | <item>#809e8974</item> 26 | <item>#80a48363</item> 27 | <item>#80a7805b</item> 28 | <item>#80af7944</item> 29 | <item>#80b47435</item> 30 | <item>#80bd8e2f</item> 31 | <item>#80c39f2b</item> 32 | <item>#80cbb526</item> 33 | <item>#80d2ca20</item> 34 | <item>#80dfb617</item> 35 | <item>#80e7a811</item> 36 | <item>#80eca00e</item> 37 | <item>#80f39409</item> 38 | <item>#80fa8804</item> 39 | <item>#80ff8000</item> 40 | <item>#80ff6a07</item> 41 | <item>#80ff4812</item> 42 | <item>#80ff2409</item> 43 | <item>#80f00000</item> 44 | </array> 45 | </resources> 46 | -------------------------------------------------------------------------------- /app/src/main/res/values/create_filter_strings.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <resources> 3 | <string name="save_and_submit_no_name_desc">Enter a name to save.</string> 4 | <string name="save_and_submit_overwrite">Overwrite existing saved filter</string> 5 | </resources> 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | <resources xmlns:tools="http://schemas.android.com/tools"> 2 | <integer tools:override="true" name="lb_details_description_body_max_lines">1000</integer> 3 | <integer tools:override="true" name="lb_details_description_body_min_lines">1000</integer> 4 | <dimen tools:override="true" name="lb_basic_card_info_padding">6dp</dimen> 5 | <dimen name="title_bar_height">74dp</dimen> 6 | <dimen name="title_bar_icon_size">54dp</dimen> 7 | <dimen name="title_bar_margin">4dp</dimen> 8 | <dimen name="title_bar_start_end_margin">5dp</dimen> 9 | <dimen name="table_text_size">13sp</dimen> 10 | <dimen name="table_text_size_large">16sp</dimen> 11 | <dimen name="scene_extra_spacing">5dp</dimen> 12 | <dimen tools:override="true" name="lb_browse_rows_margin_top">120dp</dimen> 13 | <dimen tools:override="true" name="lb_browse_padding_start">35dp</dimen> 14 | <!-- <dimen tools:override="true" name="picker_item_spacing">6dp</dimen>--> 15 | 16 | <dimen name="filter_seek_bar">250dp</dimen> 17 | <item name="alphabet_zoom" type="fraction">150%</item> 18 | <dimen name="jump_button">24dp</dimen> 19 | </resources> 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/license.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <resources> 3 | <string name="license_separator" translatable="false">\n\n================================================================================\n\n</string> 4 | <string-array name="stash_license_attributions" translatable="false"> 5 | <item>StashAppAndroidTV is distributed under AGPL-3.\nSource available at https://github.com/damontecres/StashAppAndroidTV</item> 6 | <item>Parts of https://github.com/stashapp/stash included under AGPL-3</item> 7 | </string-array> 8 | </resources> 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/values.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <resources xmlns:tools="http://schemas.android.com/tools"> 3 | <drawable name="exo_styled_controls_pause" tools:override="true">@drawable/exo_icon_pause</drawable> 4 | <drawable name="exo_styled_controls_play" tools:override="true">@drawable/exo_icon_play</drawable> 5 | </resources> 6 | -------------------------------------------------------------------------------- /app/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <paths> 3 | <external-path name="external_files" path="Download"/> 4 | </paths> 5 | -------------------------------------------------------------------------------- /app/src/test/java/android/util/Log.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Log") 2 | 3 | package android.util 4 | 5 | /** 6 | * This file provides static Log.xyz calls in test code without having to mock 7 | * 8 | * It will print the messages which is useful for debugging 9 | */ 10 | 11 | fun e( 12 | tag: String, 13 | msg: String, 14 | t: Throwable, 15 | ): Int { 16 | println("ERROR: $tag: $msg") 17 | t.printStackTrace() 18 | return 0 19 | } 20 | 21 | fun e( 22 | tag: String, 23 | msg: String, 24 | ): Int { 25 | println("ERROR: $tag: $msg") 26 | return 0 27 | } 28 | 29 | fun w( 30 | tag: String, 31 | msg: String, 32 | ): Int { 33 | println("WARN: $tag: $msg") 34 | return 0 35 | } 36 | 37 | fun i( 38 | tag: String, 39 | msg: String, 40 | ): Int { 41 | println("INFO: $tag: $msg") 42 | return 0 43 | } 44 | 45 | fun d( 46 | tag: String, 47 | msg: String, 48 | ): Int { 49 | println("DEBUG: $tag: $msg") 50 | return 0 51 | } 52 | 53 | fun v( 54 | tag: String, 55 | msg: String, 56 | ): Int { 57 | println("VERBOSE: $tag: $msg") 58 | return 0 59 | } 60 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/damontecres/stashapp/FormattingTests.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp 2 | 3 | import com.github.damontecres.stashapp.views.fileNameFromPath 4 | import org.junit.Assert 5 | import org.junit.Test 6 | 7 | class FormattingTests { 8 | @Test 9 | fun testFileNameFromPath() { 10 | Assert.assertEquals("test.zip", "/path/to/test.zip".fileNameFromPath) 11 | Assert.assertEquals("test.zip", "to/test.zip".fileNameFromPath) 12 | Assert.assertEquals("test.zip", """C:\\path\to\test.zip""".fileNameFromPath) 13 | Assert.assertEquals("test.zip", """\\192.168.1.100\path\to\test.zip""".fileNameFromPath) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/damontecres/stashapp/SortOptionTests.kt: -------------------------------------------------------------------------------- 1 | package com.github.damontecres.stashapp 2 | 3 | import com.github.damontecres.stashapp.data.SortOption 4 | import kotlinx.serialization.encodeToString 5 | import kotlinx.serialization.json.Json 6 | import org.junit.Assert 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.mockito.junit.MockitoJUnitRunner 10 | 11 | @RunWith(MockitoJUnitRunner::class) 12 | class SortOptionTests { 13 | private fun test(sortOption: SortOption) { 14 | val json = Json.encodeToString(sortOption) 15 | val result = Json.decodeFromString<SortOption>(json) 16 | Assert.assertEquals(sortOption, result) 17 | Assert.assertEquals(sortOption::class, result::class) 18 | Assert.assertEquals(sortOption.key, result.key) 19 | } 20 | 21 | @Test 22 | fun testSerialization() { 23 | test(SortOption.CreatedAt) 24 | test(SortOption.FileModTime) 25 | test(SortOption.Title) 26 | } 27 | 28 | @Test 29 | fun testSerializationUnknown() { 30 | test(SortOption.Unknown("new-key")) 31 | test(SortOption.Unknown("test test test")) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/test/resources/front_page_basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "configuration": { 4 | "ui": { 5 | "frontPageContent": [ 6 | { 7 | "__typename": "SavedFilter", 8 | "savedFilterId": 1 9 | }, 10 | { 11 | "__typename": "CustomFilter", 12 | "direction": "DESC", 13 | "message": { 14 | "id": "recently_added_objects", 15 | "values": { 16 | "objects": "Scenes" 17 | } 18 | }, 19 | "mode": "SCENES", 20 | "sortBy": "created_at" 21 | }, 22 | { 23 | "__typename": "CustomFilter", 24 | "direction": "DESC", 25 | "message": { 26 | "id": "recently_added_objects", 27 | "values": { 28 | "objects": "Performers" 29 | } 30 | }, 31 | "mode": "PERFORMERS", 32 | "sortBy": "created_at" 33 | }, 34 | { 35 | "__typename": "SavedFilter", 36 | "savedFilterId": 2 37 | } 38 | ] 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/test/resources/front_page_unsupported.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "configuration": { 4 | "ui": { 5 | "frontPageContent": [ 6 | { 7 | "__typename": "SomeNewFilterType" 8 | }, 9 | { 10 | "__typename": "CustomFilter", 11 | "direction": "DESC", 12 | "message": { 13 | "id": "recently_added_objects", 14 | "values": { 15 | "objects": "NewType" 16 | } 17 | }, 18 | "mode": "NEW_MODE", 19 | "sortBy": "created_at" 20 | } 21 | ] 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/test/resources/gender_savedfilter.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "findSavedFilter": { 4 | "id": "50", 5 | "mode": "PERFORMERS", 6 | "name": "Male-Female", 7 | "object_filter": { 8 | "gender": { 9 | "modifier": "INCLUDES", 10 | "value": [ 11 | "Male", 12 | "Female", 13 | "Non-Binary" 14 | ] 15 | } 16 | }, 17 | "__typename": "SavedFilter" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/test/resources/performer_custom_fields.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "findSavedFilter": { 4 | "id": "19", 5 | "mode": "PERFORMERS", 6 | "name": "Custom", 7 | "find_filter": { 8 | "q": "", 9 | "page": 1, 10 | "per_page": 40, 11 | "sort": "name", 12 | "direction": "ASC", 13 | "__typename": "SavedFindFilterType" 14 | }, 15 | "object_filter": { 16 | "custom_fields": [ 17 | { 18 | "field": "Field1", 19 | "modifier": "EQUALS", 20 | "value": [ 21 | "Whatever" 22 | ] 23 | }, 24 | { 25 | "field": "Field2", 26 | "modifier": "IS_NULL" 27 | }, 28 | { 29 | "field": "Field3", 30 | "modifier": "BETWEEN", 31 | "value": [ 32 | 1, 33 | 5 34 | ] 35 | } 36 | ] 37 | }, 38 | "ui_options": { 39 | "display_mode": 0, 40 | "zoom_index": 1 41 | }, 42 | "__typename": "SavedFilter" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/test/resources/studio_children_savedfilter.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "findSavedFilter": { 4 | "id": "51", 5 | "mode": "STUDIOS", 6 | "name": "Child Studios", 7 | "object_filter": { 8 | "child_count": { 9 | "modifier": "GREATER_THAN", 10 | "value": { 11 | "value": 3 12 | } 13 | } 14 | }, 15 | "__typename": "SavedFilter" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/test/resources/tag_savedfilter.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "findSavedFilter": { 4 | "id": "30", 5 | "mode": "TAGS", 6 | "name": "Test Filter", 7 | "find_filter": { 8 | "q": "", 9 | "sort": "random_109614", 10 | "direction": "ASC", 11 | "page": 1, 12 | "per_page": 40, 13 | "__typename": "SavedFindFilterType" 14 | }, 15 | "object_filter": { 16 | "aliases": { 17 | "modifier": "INCLUDES", 18 | "value": "aliases_includes" 19 | }, 20 | "children": { 21 | "modifier": "INCLUDES_ALL", 22 | "value": { 23 | "depth": -1, 24 | "excluded": [ 25 | { 26 | "id": "6", 27 | "label": "New" 28 | } 29 | ], 30 | "items": [] 31 | } 32 | }, 33 | "is_missing": { 34 | "modifier": "EQUALS", 35 | "value": "image" 36 | }, 37 | "parents": { 38 | "modifier": "INCLUDES", 39 | "value": { 40 | "depth": 0, 41 | "excluded": [], 42 | "items": [ 43 | { 44 | "id": "6", 45 | "label": "New" 46 | } 47 | ] 48 | } 49 | } 50 | }, 51 | "__typename": "SavedFilter" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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.android.application) apply false 4 | alias(libs.plugins.kotlin.android) apply false 5 | alias(libs.plugins.kotlin.jvm) apply false 6 | alias(libs.plugins.ksp) apply false 7 | kotlin("kapt") version "2.0.0" apply false 8 | alias(libs.plugins.compose.compiler) apply false 9 | } 10 | -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | repositories { 2 | mavenCentral() 3 | } 4 | 5 | plugins { 6 | alias(libs.plugins.kotlin.jvm) 7 | } 8 | 9 | java { 10 | sourceCompatibility = JavaVersion.VERSION_21 11 | targetCompatibility = JavaVersion.VERSION_21 12 | } 13 | kotlin { 14 | jvmToolchain(21) 15 | } 16 | 17 | dependencies { 18 | implementation(gradleApi()) 19 | implementation(libs.kotlinx.serialization.json) 20 | } 21 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | versionCatalogs { 3 | create("libs") { 4 | from(files("../gradle/libs.versions.toml")) 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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=-Xmx4096m -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 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damontecres/StashAppAndroidTV/4fae183567d1809898659abb346a000c26b46e65/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Mar 22 20:44:36 EDT 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven { url = uri("https://repo.repsy.io/mvn/chrynan/public") } 14 | } 15 | } 16 | 17 | rootProject.name = "StashAppAndroidTV" 18 | include(":app") 19 | 20 | gradle.startParameter.excludedTaskNames.addAll(listOf(":buildSrc:testClasses")) 21 | include(":apollo-compiler") 22 | --------------------------------------------------------------------------------