From cbd49a8798a144a92e18c3a583ccdb9032a404b5 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 19 May 2024 14:13:41 +0200 Subject: [PATCH 1/2] Use SDK types in FullDetailsFragment --- .../ui/itemdetail/FullDetailsFragment.java | 329 +++++++++--------- .../itemdetail/FullDetailsFragmentHelper.kt | 19 +- .../ui/itemhandling/ItemRowAdapter.java | 4 +- .../androidtv/util/sdk/compat/JavaCompat.kt | 36 ++ 4 files changed, 215 insertions(+), 173 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java index 25ccba3118..06b36a89f9 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java @@ -87,11 +87,7 @@ import org.jellyfin.androidtv.util.sdk.compat.JavaCompat; import org.jellyfin.androidtv.util.sdk.compat.ModelCompat; import org.jellyfin.apiclient.interaction.ApiClient; -import org.jellyfin.apiclient.model.dto.BaseItemDto; -import org.jellyfin.apiclient.model.dto.BaseItemType; -import org.jellyfin.apiclient.model.dto.MediaSourceInfo; import org.jellyfin.apiclient.model.dto.UserItemDataDto; -import org.jellyfin.apiclient.model.entities.MediaStream; import org.jellyfin.apiclient.model.livetv.ChannelInfoDto; import org.jellyfin.apiclient.model.livetv.TimerQuery; import org.jellyfin.apiclient.model.querying.EpisodeQuery; @@ -102,9 +98,12 @@ import org.jellyfin.apiclient.model.querying.SeasonQuery; import org.jellyfin.apiclient.model.querying.SimilarItemsQuery; import org.jellyfin.apiclient.model.querying.UpcomingEpisodesQuery; +import org.jellyfin.sdk.model.api.BaseItemDto; import org.jellyfin.sdk.model.api.BaseItemKind; import org.jellyfin.sdk.model.api.BaseItemPerson; import org.jellyfin.sdk.model.api.ItemSortBy; +import org.jellyfin.sdk.model.api.MediaSourceInfo; +import org.jellyfin.sdk.model.api.MediaStream; import org.jellyfin.sdk.model.api.MediaType; import org.jellyfin.sdk.model.api.PersonKind; import org.jellyfin.sdk.model.api.SeriesTimerInfoDto; @@ -113,7 +112,8 @@ import org.koin.java.KoinJavaComponent; import java.time.Instant; -import java.time.ZoneId; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -139,7 +139,7 @@ public class FullDetailsFragment extends Fragment implements RecordingIndicatorV private DisplayMetrics mMetrics; - protected org.jellyfin.sdk.model.api.BaseItemDto mProgramInfo; + protected BaseItemDto mProgramInfo; protected SeriesTimerInfoDto mSeriesTimerInfo; protected UUID mItemId; protected UUID mChannelId; @@ -198,7 +198,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c mChannelId = Utils.uuidOrNull(getArguments().getString("ChannelId")); String programJson = getArguments().getString("ProgramInfo"); if (programJson != null) { - mProgramInfo =Json.Default.decodeFromString(org.jellyfin.sdk.model.api.BaseItemDto.Companion.serializer(), programJson); + mProgramInfo =Json.Default.decodeFromString(BaseItemDto.Companion.serializer(), programJson); } String timerJson = getArguments().getString("SeriesTimer"); if (timerJson != null) { @@ -206,7 +206,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c } CoroutineUtils.readCustomMessagesOnLifecycle(getLifecycle(), customMessageRepository.getValue(), message -> { - if (message.equals(CustomMessage.ActionComplete.INSTANCE) && mSeriesTimerInfo != null && mBaseItem.getBaseItemType() == BaseItemType.SeriesTimer) { + if (message.equals(CustomMessage.ActionComplete.INSTANCE) && mSeriesTimerInfo != null) { //update info apiClient.getValue().GetLiveTvSeriesTimerAsync(mSeriesTimerInfo.getId(), new LifecycleAwareResponse(getLifecycle()) { @Override @@ -214,7 +214,7 @@ public void onResponse(org.jellyfin.apiclient.model.livetv.SeriesTimerInfoDto re if (!getActive()) return; mSeriesTimerInfo = ModelCompat.asSdk(response); - mBaseItem.setOverview(BaseItemUtils.getSeriesOverview(mSeriesTimerInfo, requireContext())); + mBaseItem = JavaCompat.copyWithOverview(mBaseItem, BaseItemUtils.getSeriesOverview(mSeriesTimerInfo, requireContext())); mDorPresenter.getViewHolder().setSummary(mBaseItem.getOverview()); } }); @@ -269,22 +269,22 @@ public void run() { // if last playback event exists, and event time is greater than last sync or within 2 seconds of current time // the third condition accounts for a situation where a sync (dataRefresh) coincides with the end of playback - if (lastPlaybackTime != null && (lastPlaybackTime.isAfter(mLastUpdated) || Instant.now().toEpochMilli() - lastPlaybackTime.toEpochMilli() < 2000) && ModelCompat.asSdk(mBaseItem).getType() != BaseItemKind.MUSIC_ARTIST) { - org.jellyfin.sdk.model.api.BaseItemDto lastPlayedItem = dataRefreshService.getValue().getLastPlayedItem(); - if (ModelCompat.asSdk(mBaseItem).getType() == BaseItemKind.EPISODE && lastPlayedItem != null && !mBaseItem.getId().equals(lastPlayedItem.getId().toString()) && lastPlayedItem.getType() == BaseItemKind.EPISODE) { + if (lastPlaybackTime != null && (lastPlaybackTime.isAfter(mLastUpdated) || Instant.now().toEpochMilli() - lastPlaybackTime.toEpochMilli() < 2000) && mBaseItem.getType() != BaseItemKind.MUSIC_ARTIST) { + BaseItemDto lastPlayedItem = dataRefreshService.getValue().getLastPlayedItem(); + if (mBaseItem.getType() == BaseItemKind.EPISODE && lastPlayedItem != null && !mBaseItem.getId().equals(lastPlayedItem.getId().toString()) && lastPlayedItem.getType() == BaseItemKind.EPISODE) { Timber.i("Re-loading after new episode playback"); loadItem(lastPlayedItem.getId()); dataRefreshService.getValue().setLastPlayedItem(null); //blank this out so a detail screen we back up to doesn't also do this } else { Timber.d("Updating info after playback"); - apiClient.getValue().GetItemAsync(mBaseItem.getId(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { + apiClient.getValue().GetItemAsync(mBaseItem.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { @Override - public void onResponse(BaseItemDto response) { + public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { if (!getActive()) return; - mBaseItem = response; + mBaseItem = ModelCompat.asSdk(response); if (mResumeButton != null) { - boolean resumeVisible = (ModelCompat.asSdk(mBaseItem).getType() == BaseItemKind.SERIES && !mBaseItem.getUserData().getPlayed()) || response.getCanResume(); + boolean resumeVisible = (mBaseItem.getType() == BaseItemKind.SERIES && !mBaseItem.getUserData().getPlayed()) || response.getCanResume(); mResumeButton.setVisibility(resumeVisible ? View.VISIBLE : View.GONE); if (response.getCanResume()) { mResumeButton.setLabel(getString(R.string.lbl_resume_from, TimeUtils.formatMillis((response.getUserData().getPlaybackPositionTicks()/10000) - getResumePreroll()))); @@ -324,7 +324,7 @@ public boolean onKey(View v, int keyCode, KeyEvent event) { if (mCurrentItem != null) { return keyProcessor.getValue().handleKey(keyCode, mCurrentItem, requireActivity()); - } else if ((keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) && BaseItemExtensionsKt.canPlay(ModelCompat.asSdk(mBaseItem))) { + } else if ((keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) && BaseItemExtensionsKt.canPlay(mBaseItem)) { //default play action Long pos = mBaseItem.getUserData().getPlaybackPositionTicks() / 10000; play(mBaseItem, pos.intValue() , false); @@ -344,7 +344,7 @@ public void run() { MyDetailsOverviewRowPresenter.ViewHolder viewholder = mDorPresenter.getViewHolder(); if (viewholder == null) return; - if (mBaseItem != null && ((mBaseItem.getRunTimeTicks() != null && mBaseItem.getRunTimeTicks() > 0) || mBaseItem.getOriginalRunTimeTicks() != null)) { + if (mBaseItem != null && ((mBaseItem.getRunTimeTicks() != null && mBaseItem.getRunTimeTicks() > 0) || mBaseItem.getRunTimeTicks() != null)) { viewholder.setInfoValue3(getEndTime()); mLoopHandler.postDelayed(this, 15000); } @@ -393,33 +393,25 @@ public void onResponse(ChannelInfoDto response) { mProgramInfo = ModelCompat.asSdk(response.getCurrentProgram()); mItemId = mProgramInfo.getId(); - apiClient.getValue().GetItemAsync(mItemId.toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { + apiClient.getValue().GetItemAsync(mItemId.toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { @Override - public void onResponse(BaseItemDto response) { + public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { if (!getActive()) return; - setBaseItem(response); + setBaseItem(ModelCompat.asSdk(response)); } }); } }); } else if (mSeriesTimerInfo != null) { - // create base item from our timer - BaseItemDto item = new BaseItemDto(); - item.setId(mSeriesTimerInfo.getId()); - item.setBaseItemType(BaseItemType.Folder); - item.setSeriesTimerId(mSeriesTimerInfo.getId()); - item.setName(mSeriesTimerInfo.getName()); - item.setOverview(BaseItemUtils.getSeriesOverview(mSeriesTimerInfo, requireContext())); - - setBaseItem(item); + setBaseItem(FullDetailsFragmentHelperKt.createFakeSeriesTimerBaseItemDto(this, mSeriesTimerInfo)); } else { - apiClient.getValue().GetItemAsync(id.toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { + apiClient.getValue().GetItemAsync(id.toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { @Override - public void onResponse(BaseItemDto response) { + public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { if (!getActive()) return; - setBaseItem(response); + setBaseItem(ModelCompat.asSdk(response)); } @Override @@ -458,29 +450,29 @@ protected MyDetailsOverviewRow doInBackground(BaseItemDto... params) { BaseItemDto item = params[0]; // Figure image size - Double aspect = imageHelper.getValue().getImageAspectRatio(ModelCompat.asSdk(item), false); - posterHeight = aspect > 1 ? Utils.convertDpToPixel(requireContext(), 160) : Utils.convertDpToPixel(requireContext(), ModelCompat.asSdk(item).getType() == BaseItemKind.PERSON || ModelCompat.asSdk(item).getType() == BaseItemKind.MUSIC_ARTIST ? 300 : 200); + Double aspect = imageHelper.getValue().getImageAspectRatio(item, false); + posterHeight = aspect > 1 ? Utils.convertDpToPixel(requireContext(), 160) : Utils.convertDpToPixel(requireContext(), item.getType() == BaseItemKind.PERSON || item.getType() == BaseItemKind.MUSIC_ARTIST ? 300 : 200); - mDetailsOverviewRow = new MyDetailsOverviewRow(ModelCompat.asSdk(item)); + mDetailsOverviewRow = new MyDetailsOverviewRow(item); - String primaryImageUrl = imageHelper.getValue().getLogoImageUrl(ModelCompat.asSdk(mBaseItem), 600, true); + String primaryImageUrl = imageHelper.getValue().getLogoImageUrl(mBaseItem, 600, true); if (primaryImageUrl == null) { - primaryImageUrl = imageHelper.getValue().getPrimaryImageUrl(ModelCompat.asSdk(mBaseItem), false, null, posterHeight); + primaryImageUrl = imageHelper.getValue().getPrimaryImageUrl(mBaseItem, false, null, posterHeight); if (item.getRunTimeTicks() != null && item.getRunTimeTicks() > 0 && item.getUserData() != null && item.getUserData().getPlaybackPositionTicks() > 0) mDetailsOverviewRow.setProgress(((int) (item.getUserData().getPlaybackPositionTicks() * 100.0 / item.getRunTimeTicks()))); } mDetailsOverviewRow.setSummary(item.getOverview()); - switch (item.getBaseItemType()) { - case Person: - case MusicArtist: + switch (item.getType()) { + case PERSON: + case MUSIC_ARTIST: break; default: - BaseItemPerson director = BaseItemExtensionsKt.getFirstPerson(ModelCompat.asSdk(item), PersonKind.DIRECTOR); + BaseItemPerson director = BaseItemExtensionsKt.getFirstPerson(item, PersonKind.DIRECTOR); InfoItem firstRow; - if (ModelCompat.asSdk(item).getType() == BaseItemKind.SERIES) { + if (item.getType() == BaseItemKind.SERIES) { firstRow = new InfoItem( getString(R.string.lbl_seasons), String.format("%d", Utils.getSafeValue(item.getChildCount(), 0))); @@ -491,7 +483,7 @@ protected MyDetailsOverviewRow doInBackground(BaseItemDto... params) { } mDetailsOverviewRow.setInfoItem1(firstRow); - if ((item.getRunTimeTicks() != null && item.getRunTimeTicks() > 0) || item.getOriginalRunTimeTicks() != null) { + if ((item.getRunTimeTicks() != null && item.getRunTimeTicks() > 0) || item.getRunTimeTicks() != null) { mDetailsOverviewRow.setInfoItem2(new InfoItem(getString(R.string.lbl_runs), getRunTime())); ClockBehavior clockBehavior = userPreferences.getValue().get(UserPreferences.Companion.getClockBehavior()); if (clockBehavior == ClockBehavior.ALWAYS || clockBehavior == ClockBehavior.IN_MENUS) { @@ -533,13 +525,17 @@ protected void onPostExecute(MyDetailsOverviewRow detailsOverviewRow) { public void setBaseItem(BaseItemDto item) { mBaseItem = item; - backgroundService.getValue().setBackground(ModelCompat.asSdk(item)); + backgroundService.getValue().setBackground(item); if (mBaseItem != null) { if (mChannelId != null) { - mBaseItem.setParentId(mChannelId.toString()); - mBaseItem.setPremiereDate(TimeUtils.getDate(mProgramInfo.getStartDate())); - mBaseItem.setEndDate(TimeUtils.getDate(mProgramInfo.getEndDate(), ZoneId.systemDefault())); - mBaseItem.setRunTimeTicks(mProgramInfo.getRunTimeTicks()); + mBaseItem = JavaCompat.copyWithParentId(mBaseItem, mChannelId); + mBaseItem = JavaCompat.copyWithDates( + mBaseItem, + mProgramInfo.getStartDate(), + mProgramInfo.getEndDate(), + mBaseItem.getOfficialRating(), + mProgramInfo.getRunTimeTicks() + ); } new BuildDorTask().execute(item); } @@ -554,35 +550,43 @@ protected void addItemRow(MutableObjectAdapter parent, ItemRowAdapter row, } protected void addAdditionalRows(MutableObjectAdapter adapter) { - Timber.d("Item type: %s", mBaseItem.getBaseItemType().toString()); - switch (mBaseItem.getBaseItemType()) { - case Movie: + Timber.d("Item type: %s", mBaseItem.getType().toString()); + + if (mSeriesTimerInfo != null) { + TimerQuery scheduled = new TimerQuery(); + scheduled.setSeriesTimerId(mSeriesTimerInfo.getId()); + TvManager.getScheduleRowsAsync(requireContext(), scheduled, new CardPresenter(true), adapter, new LifecycleAwareResponse(getLifecycle()) { }); + return; + } + + switch (mBaseItem.getType()) { + case MOVIE: //Additional Parts if (mBaseItem.getPartCount() != null && mBaseItem.getPartCount() > 0) { - ItemRowAdapter additionalPartsAdapter = new ItemRowAdapter(requireContext(), new AdditionalPartsQuery(mBaseItem.getId()), new CardPresenter(), adapter); + ItemRowAdapter additionalPartsAdapter = new ItemRowAdapter(requireContext(), new AdditionalPartsQuery(mBaseItem.getId().toString()), new CardPresenter(), adapter); addItemRow(adapter, additionalPartsAdapter, 0, getString(R.string.lbl_additional_parts)); } //Cast/Crew - if (mBaseItem.getPeople() != null && mBaseItem.getPeople().length > 0) { - ItemRowAdapter castAdapter = new ItemRowAdapter(requireContext(), ModelCompat.asSdk(mBaseItem.getPeople()), new CardPresenter(true, 130), adapter); + if (mBaseItem.getPeople() != null && !mBaseItem.getPeople().isEmpty()) { + ItemRowAdapter castAdapter = new ItemRowAdapter(mBaseItem.getPeople(), requireContext(), new CardPresenter(true, 130), adapter); addItemRow(adapter, castAdapter, 1, getString(R.string.lbl_cast_crew)); } //Specials if (mBaseItem.getSpecialFeatureCount() != null && mBaseItem.getSpecialFeatureCount() > 0) { - addItemRow(adapter, new ItemRowAdapter(requireContext(), new SpecialsQuery(mBaseItem.getId()), new CardPresenter(), adapter), 3, getString(R.string.lbl_specials)); + addItemRow(adapter, new ItemRowAdapter(requireContext(), new SpecialsQuery(mBaseItem.getId().toString()), new CardPresenter(), adapter), 3, getString(R.string.lbl_specials)); } //Trailers if (mBaseItem.getLocalTrailerCount() != null && mBaseItem.getLocalTrailerCount() > 1) { - addItemRow(adapter, new ItemRowAdapter(requireContext(), new TrailersQuery(mBaseItem.getId()), new CardPresenter(), adapter), 4, getString(R.string.lbl_trailers)); + addItemRow(adapter, new ItemRowAdapter(requireContext(), new TrailersQuery(mBaseItem.getId().toString()), new CardPresenter(), adapter), 4, getString(R.string.lbl_trailers)); } //Chapters - if (mBaseItem.getChapters() != null && mBaseItem.getChapters().size() > 0) { - List chapters = BaseItemExtensionsKt.buildChapterItems(ModelCompat.asSdk(mBaseItem), api.getValue()); + if (mBaseItem.getChapters() != null && !mBaseItem.getChapters().isEmpty()) { + List chapters = BaseItemExtensionsKt.buildChapterItems(mBaseItem, api.getValue()); ItemRowAdapter chapterAdapter = new ItemRowAdapter(requireContext(), chapters, new CardPresenter(true, 120), adapter); addItemRow(adapter, chapterAdapter, 2, getString(R.string.lbl_chapters)); } @@ -594,7 +598,7 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { ItemFields.ChildCount }); similar.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - similar.setId(mBaseItem.getId()); + similar.setId(mBaseItem.getId().toString()); similar.setLimit(10); ItemRowAdapter similarMoviesAdapter = new ItemRowAdapter(requireContext(), similar, QueryType.SimilarMovies, new CardPresenter(), adapter); @@ -602,11 +606,11 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { addInfoRows(adapter); break; - case Trailer: + case TRAILER: //Cast/Crew - if (mBaseItem.getPeople() != null && mBaseItem.getPeople().length > 0) { - ItemRowAdapter castAdapter = new ItemRowAdapter(requireContext(), ModelCompat.asSdk(mBaseItem.getPeople()), new CardPresenter(true, 130), adapter); + if (mBaseItem.getPeople() != null && !mBaseItem.getPeople().isEmpty()) { + ItemRowAdapter castAdapter = new ItemRowAdapter(mBaseItem.getPeople(), requireContext(), new CardPresenter(true, 130), adapter); addItemRow(adapter, castAdapter, 0, getString(R.string.lbl_cast_crew)); } @@ -617,14 +621,14 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { ItemFields.ChildCount }); similarTrailer.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - similarTrailer.setId(mBaseItem.getId()); + similarTrailer.setId(mBaseItem.getId().toString()); similarTrailer.setLimit(10); ItemRowAdapter similarTrailerAdapter = new ItemRowAdapter(requireContext(), similarTrailer, QueryType.SimilarMovies, new CardPresenter(), adapter); addItemRow(adapter, similarTrailerAdapter, 4, getString(R.string.lbl_more_like_this)); addInfoRows(adapter); break; - case Person: + case PERSON: ItemQuery personMovies = new ItemQuery(); personMovies.setFields(new ItemFields[]{ @@ -632,7 +636,7 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { ItemFields.ChildCount }); personMovies.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - personMovies.setPersonIds(new String[] {mBaseItem.getId()}); + personMovies.setPersonIds(new String[] {mBaseItem.getId().toString()}); personMovies.setRecursive(true); personMovies.setIncludeItemTypes(new String[] {"Movie"}); personMovies.setSortBy(new String[] {ItemSortBy.SORT_NAME.getSerialName()}); @@ -646,7 +650,7 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { ItemFields.ChildCount }); personSeries.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - personSeries.setPersonIds(new String[] {mBaseItem.getId()}); + personSeries.setPersonIds(new String[] {mBaseItem.getId().toString()}); personSeries.setRecursive(true); personSeries.setIncludeItemTypes(new String[] {"Series"}); personSeries.setSortBy(new String[] {ItemSortBy.SORT_NAME.getSerialName()}); @@ -660,7 +664,7 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { ItemFields.ChildCount }); personEpisodes.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - personEpisodes.setPersonIds(new String[] {mBaseItem.getId()}); + personEpisodes.setPersonIds(new String[] {mBaseItem.getId().toString()}); personEpisodes.setRecursive(true); personEpisodes.setIncludeItemTypes(new String[] {"Episode"}); personEpisodes.setSortBy(new String[] {ItemSortBy.SERIES_SORT_NAME.getSerialName(), ItemSortBy.SORT_NAME.getSerialName()}); @@ -668,7 +672,7 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { addItemRow(adapter, personEpisodesAdapter, 2, getString(R.string.lbl_episodes)); break; - case MusicArtist: + case MUSIC_ARTIST: ItemQuery artistAlbums = new ItemQuery(); artistAlbums.setFields(new ItemFields[]{ @@ -676,17 +680,17 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { ItemFields.ChildCount }); artistAlbums.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - artistAlbums.setArtistIds(new String[]{mBaseItem.getId()}); + artistAlbums.setArtistIds(new String[]{mBaseItem.getId().toString()}); artistAlbums.setRecursive(true); artistAlbums.setIncludeItemTypes(new String[]{"MusicAlbum"}); ItemRowAdapter artistAlbumsAdapter = new ItemRowAdapter(requireContext(), artistAlbums, 100, false, new CardPresenter(), adapter); addItemRow(adapter, artistAlbumsAdapter, 0, getString(R.string.lbl_albums)); break; - case Series: + case SERIES: NextUpQuery nextUpQuery = new NextUpQuery(); nextUpQuery.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - nextUpQuery.setSeriesId(mBaseItem.getId()); + nextUpQuery.setSeriesId(mBaseItem.getId().toString()); nextUpQuery.setFields(new ItemFields[]{ ItemFields.PrimaryImageAspectRatio, ItemFields.ChildCount @@ -695,7 +699,7 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { addItemRow(adapter, nextUpAdapter, 0, getString(R.string.lbl_next_up)); SeasonQuery seasons = new SeasonQuery(); - seasons.setSeriesId(mBaseItem.getId()); + seasons.setSeriesId(mBaseItem.getId().toString()); seasons.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); seasons.setFields(new ItemFields[] { ItemFields.PrimaryImageAspectRatio, @@ -707,12 +711,12 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { //Specials if (mBaseItem.getSpecialFeatureCount() != null && mBaseItem.getSpecialFeatureCount() > 0) { - addItemRow(adapter, new ItemRowAdapter(requireContext(), new SpecialsQuery(mBaseItem.getId()), new CardPresenter(), adapter), 3, getString(R.string.lbl_specials)); + addItemRow(adapter, new ItemRowAdapter(requireContext(), new SpecialsQuery(mBaseItem.getId().toString()), new CardPresenter(), adapter), 3, getString(R.string.lbl_specials)); } UpcomingEpisodesQuery upcoming = new UpcomingEpisodesQuery(); upcoming.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - upcoming.setParentId(mBaseItem.getId()); + upcoming.setParentId(mBaseItem.getId().toString()); upcoming.setFields(new ItemFields[]{ ItemFields.PrimaryImageAspectRatio, ItemFields.ChildCount @@ -720,8 +724,8 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { ItemRowAdapter upcomingAdapter = new ItemRowAdapter(requireContext(), upcoming, new CardPresenter(), adapter); addItemRow(adapter, upcomingAdapter, 2, getString(R.string.lbl_upcoming)); - if (mBaseItem.getPeople() != null && mBaseItem.getPeople().length > 0) { - ItemRowAdapter seriesCastAdapter = new ItemRowAdapter(requireContext(), ModelCompat.asSdk(mBaseItem.getPeople()), new CardPresenter(true, 130), adapter); + if (mBaseItem.getPeople() != null && !mBaseItem.getPeople().isEmpty()) { + ItemRowAdapter seriesCastAdapter = new ItemRowAdapter(mBaseItem.getPeople(), requireContext(), new CardPresenter(true, 130), adapter); addItemRow(adapter, seriesCastAdapter, 3, getString(R.string.lbl_cast_crew)); } @@ -733,16 +737,16 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { ItemFields.ChildCount }); similarSeries.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - similarSeries.setId(mBaseItem.getId()); + similarSeries.setId(mBaseItem.getId().toString()); similarSeries.setLimit(20); ItemRowAdapter similarAdapter = new ItemRowAdapter(requireContext(), similarSeries, QueryType.SimilarSeries, new CardPresenter(), adapter); addItemRow(adapter, similarAdapter, 4, getString(R.string.lbl_more_like_this)); break; - case Episode: + case EPISODE: if (mBaseItem.getSeasonId() != null && mBaseItem.getIndexNumber() != null) { StdItemQuery nextEpisodes = new StdItemQuery(); - nextEpisodes.setParentId(mBaseItem.getSeasonId()); + nextEpisodes.setParentId(mBaseItem.getSeasonId().toString()); nextEpisodes.setIncludeItemTypes(new String[]{"Episode"}); nextEpisodes.setStartIndex(mBaseItem.getIndexNumber()); // query index is zero-based but episode no is not nextEpisodes.setLimit(20); @@ -751,45 +755,37 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { } //Guest stars - if (mBaseItem.getPeople() != null && mBaseItem.getPeople().length > 0) { + if (mBaseItem.getPeople() != null && !mBaseItem.getPeople().isEmpty()) { List guests = new ArrayList<>(); - for (BaseItemPerson person : ModelCompat.asSdk(mBaseItem.getPeople())) { + for (BaseItemPerson person : mBaseItem.getPeople()) { if (person.getType() == PersonKind.GUEST_STAR) guests.add(person); } - if (guests.size() > 0) { - ItemRowAdapter castAdapter = new ItemRowAdapter(requireContext(), guests.toArray(new BaseItemPerson[guests.size()]), new CardPresenter(true, 130), adapter); + if (!guests.isEmpty()) { + ItemRowAdapter castAdapter = new ItemRowAdapter(guests, requireContext(), new CardPresenter(true, 130), adapter); addItemRow(adapter, castAdapter, 0, getString(R.string.lbl_guest_stars)); } } //Chapters - if (mBaseItem.getChapters() != null && mBaseItem.getChapters().size() > 0) { - List chapters = BaseItemExtensionsKt.buildChapterItems(ModelCompat.asSdk(mBaseItem), api.getValue()); + if (mBaseItem.getChapters() != null && !mBaseItem.getChapters().isEmpty()) { + List chapters = BaseItemExtensionsKt.buildChapterItems(mBaseItem, api.getValue()); ItemRowAdapter chapterAdapter = new ItemRowAdapter(requireContext(), chapters, new CardPresenter(true, 120), adapter); addItemRow(adapter, chapterAdapter, 1, getString(R.string.lbl_chapters)); } addInfoRows(adapter); break; - - case SeriesTimer: - TimerQuery scheduled = new TimerQuery(); - scheduled.setSeriesTimerId(mSeriesTimerInfo.getId()); - TvManager.getScheduleRowsAsync(requireContext(), scheduled, new CardPresenter(true), adapter, new LifecycleAwareResponse(getLifecycle()) {}); - break; } - - } private void addInfoRows(MutableObjectAdapter adapter) { if (KoinJavaComponent.get(UserPreferences.class).get(UserPreferences.Companion.getDebuggingEnabled()) && mBaseItem.getMediaSources() != null) { for (MediaSourceInfo ms : mBaseItem.getMediaSources()) { - if (ms.getMediaStreams() != null && ms.getMediaStreams().size() > 0) { + if (ms.getMediaStreams() != null && !ms.getMediaStreams().isEmpty()) { HeaderItem header = new HeaderItem("Media Details"+(ms.getContainer() != null ? " (" +ms.getContainer()+")" : "")); ArrayObjectAdapter infoAdapter = new ArrayObjectAdapter(new InfoCardPresenter()); for (MediaStream stream : ms.getMediaStreams()) { - infoAdapter.add(ModelCompat.asSdk(stream)); + infoAdapter.add(stream); } adapter.add(new ListRow(header, infoAdapter)); @@ -799,7 +795,7 @@ private void addInfoRows(MutableObjectAdapter adapter) { } } - private void updateInfo(org.jellyfin.sdk.model.api.BaseItemDto item) { + private void updateInfo(BaseItemDto item) { if (buttonTypeList.contains(item.getType())) addButtons(BUTTON_SIZE); mLastUpdated = Instant.now(); @@ -812,7 +808,7 @@ public void setTitle(String title) { void playTrailers() { // External trailer if (mBaseItem.getLocalTrailerCount() == null || mBaseItem.getLocalTrailerCount() < 1) { - Intent intent = TrailerUtils.getExternalTrailerIntent(requireContext(), ModelCompat.asSdk(mBaseItem)); + Intent intent = TrailerUtils.getExternalTrailerIntent(requireContext(), mBaseItem); try { startActivity(intent); @@ -825,12 +821,12 @@ void playTrailers() { } // Local trailer - apiClient.getValue().GetLocalTrailersAsync(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), mBaseItem.getId(), new LifecycleAwareResponse(getLifecycle()) { + apiClient.getValue().GetLocalTrailersAsync(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), mBaseItem.getId().toString(), new LifecycleAwareResponse(getLifecycle()) { @Override - public void onResponse(BaseItemDto[] response) { + public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto[] response) { if (!getActive()) return; - play(response, 0, false); + play(JavaCompat.mapBaseItemArray(response), 0, false); } @Override @@ -844,16 +840,16 @@ public void onError(Exception exception) { } private String getRunTime() { - Long runtime = Utils.getSafeValue(mBaseItem.getRunTimeTicks(), mBaseItem.getOriginalRunTimeTicks()); + Long runtime = Utils.getSafeValue(mBaseItem.getRunTimeTicks(), mBaseItem.getRunTimeTicks()); return runtime != null && runtime > 0 ? String.format("%d%s", (int) Math.ceil((double) runtime / 600000000), getString(R.string.lbl_min)) : ""; } private String getEndTime() { - if (mBaseItem != null && ModelCompat.asSdk(mBaseItem).getType() != BaseItemKind.MUSIC_ARTIST && ModelCompat.asSdk(mBaseItem).getType() != BaseItemKind.PERSON) { - Long runtime = Utils.getSafeValue(mBaseItem.getRunTimeTicks(), mBaseItem.getOriginalRunTimeTicks()); + if (mBaseItem != null && mBaseItem.getType() != BaseItemKind.MUSIC_ARTIST && mBaseItem.getType() != BaseItemKind.PERSON) { + Long runtime = Utils.getSafeValue(mBaseItem.getRunTimeTicks(), mBaseItem.getRunTimeTicks()); if (runtime != null && runtime > 0) { - long endTimeTicks = ModelCompat.asSdk(mBaseItem).getType() == BaseItemKind.PROGRAM && mBaseItem.getEndDate() != null ? TimeUtils.convertToLocalDate(mBaseItem.getEndDate()).getTime() : Instant.now().toEpochMilli() + runtime / 10000; - if (mBaseItem.getCanResume()) { + long endTimeTicks = mBaseItem.getType() == BaseItemKind.PROGRAM && mBaseItem.getEndDate() != null ? mBaseItem.getEndDate().toInstant(ZoneOffset.UTC).toEpochMilli() : Instant.now().toEpochMilli() + runtime / 10000; + if (JavaCompat.getCanResume(mBaseItem)) { endTimeTicks = Instant.now().toEpochMilli() + ((runtime - mBaseItem.getUserData().getPlaybackPositionTicks()) / 10000); } return android.text.format.DateFormat.getTimeFormat(requireContext()).format(new Date(endTimeTicks)); @@ -864,12 +860,12 @@ private String getEndTime() { } void addItemToQueue() { - org.jellyfin.sdk.model.api.BaseItemDto baseItem = ModelCompat.asSdk(mBaseItem); + BaseItemDto baseItem = mBaseItem; if (baseItem.getType() == BaseItemKind.AUDIO || baseItem.getType() == BaseItemKind.MUSIC_ALBUM || baseItem.getType() == BaseItemKind.MUSIC_ARTIST) { if (baseItem.getType() == BaseItemKind.MUSIC_ALBUM || baseItem.getType() == BaseItemKind.MUSIC_ARTIST) { - playbackHelper.getValue().getItemsToPlay(getContext(), baseItem, false, false, new LifecycleAwareResponse>(getLifecycle()) { + playbackHelper.getValue().getItemsToPlay(getContext(), baseItem, false, false, new LifecycleAwareResponse>(getLifecycle()) { @Override - public void onResponse(List response) { + public void onResponse(List response) { if (!getActive()) return; mediaManager.getValue().addToAudioQueue(response); @@ -882,13 +878,13 @@ public void onResponse(List response) { } void toggleFavorite() { - UserItemDataDto data = mBaseItem.getUserData(); - apiClient.getValue().UpdateFavoriteStatusAsync(mBaseItem.getId(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), !data.getIsFavorite(), new LifecycleAwareResponse(getLifecycle()) { + org.jellyfin.sdk.model.api.UserItemDataDto data = mBaseItem.getUserData(); + apiClient.getValue().UpdateFavoriteStatusAsync(mBaseItem.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), !data.isFavorite(), new LifecycleAwareResponse(getLifecycle()) { @Override public void onResponse(UserItemDataDto response) { if (!getActive()) return; - mBaseItem.setUserData(response); + mBaseItem = JavaCompat.copyWithUserData(mBaseItem, ModelCompat.asSdk(response)); favButton.setActivated(response.getIsFavorite()); dataRefreshService.getValue().setLastFavoriteUpdate(Instant.now()); } @@ -896,7 +892,7 @@ public void onResponse(UserItemDataDto response) { } void gotoSeries() { - navigationRepository.getValue().navigate(Destinations.INSTANCE.itemDetails(UUIDSerializerKt.toUUID(mBaseItem.getSeriesId()))); + navigationRepository.getValue().navigate(Destinations.INSTANCE.itemDetails(mBaseItem.getSeriesId())); } private void deleteItem() { @@ -909,7 +905,7 @@ private void deleteItem() { FullDetailsFragmentHelperKt.deleteItem( this, api.getValue(), - ModelCompat.asSdk(mBaseItem), + mBaseItem, dataRefreshService.getValue(), navigationRepository.getValue() ); @@ -927,13 +923,13 @@ private void deleteItem() { TextUnderButton trailerButton = null; private void addButtons(int buttonSize) { - org.jellyfin.sdk.model.api.BaseItemDto baseItem = ModelCompat.asSdk(mBaseItem); + BaseItemDto baseItem = mBaseItem; String buttonLabel; if (baseItem.getType() == BaseItemKind.SERIES) { buttonLabel = getString(R.string.lbl_play_next_up); } else { long startPos = 0; - if (mBaseItem.getCanResume()) { + if (JavaCompat.getCanResume(mBaseItem)) { startPos = (mBaseItem.getUserData().getPlaybackPositionTicks()/10000) - getResumePreroll(); } buttonLabel = getString(R.string.lbl_resume_from, TimeUtils.formatMillis(startPos)); @@ -945,14 +941,14 @@ public void onClick(View v) { //play next up NextUpQuery nextUpQuery = new NextUpQuery(); nextUpQuery.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - nextUpQuery.setSeriesId(mBaseItem.getId()); + nextUpQuery.setSeriesId(mBaseItem.getId().toString()); apiClient.getValue().GetNextUpEpisodesAsync(nextUpQuery, new LifecycleAwareResponse(getLifecycle()) { @Override public void onResponse(ItemsResult response) { if (!getActive()) return; if (response.getItems().length > 0) { - play(response.getItems()[0], 0 , false); + play(ModelCompat.asSdk(response.getItems()[0]), 0 , false); } else { Utils.showToast(requireContext(), "Unable to find next up episode"); } @@ -985,7 +981,7 @@ public void onClick(View view) { }); mDetailsOverviewRow.addAction(playButton); - if (mBaseItem.getIsFolderItem()) { + if (mBaseItem.isFolder()) { shuffleButton = TextUnderButton.create(requireContext(), R.drawable.ic_shuffle, buttonSize, 2, getString(R.string.lbl_shuffle_all), new View.OnClickListener() { @Override public void onClick(View view) { @@ -997,10 +993,10 @@ public void onClick(View view) { } else { //here playButton is only a play button if (BaseItemExtensionsKt.canPlay(baseItem)) { mDetailsOverviewRow.addAction(mResumeButton); - boolean resumeButtonVisible = (baseItem.getType() == BaseItemKind.SERIES && !mBaseItem.getUserData().getPlayed()) || (mBaseItem.getCanResume()); + boolean resumeButtonVisible = (baseItem.getType() == BaseItemKind.SERIES && !mBaseItem.getUserData().getPlayed()) || (JavaCompat.getCanResume(mBaseItem)); mResumeButton.setVisibility(resumeButtonVisible ? View.VISIBLE : View.GONE); - playButton = TextUnderButton.create(requireContext(), R.drawable.ic_play, buttonSize, 2, getString(BaseItemExtensionsKt.isLiveTv(ModelCompat.asSdk(mBaseItem)) ? R.string.lbl_tune_to_channel : mBaseItem.getIsFolderItem() ? R.string.lbl_play_all : R.string.lbl_play), new View.OnClickListener() { + playButton = TextUnderButton.create(requireContext(), R.drawable.ic_play, buttonSize, 2, getString(BaseItemExtensionsKt.isLiveTv(mBaseItem) ? R.string.lbl_tune_to_channel : mBaseItem.isFolder() ? R.string.lbl_play_all : R.string.lbl_play), new View.OnClickListener() { @Override public void onClick(View v) { play(mBaseItem, 0, false); @@ -1030,7 +1026,7 @@ public void onClick(View v) { mDetailsOverviewRow.addAction(queueButton); } - if (mBaseItem.getIsFolderItem() || baseItem.getType() == BaseItemKind.MUSIC_ARTIST) { + if (mBaseItem.isFolder() || baseItem.getType() == BaseItemKind.MUSIC_ARTIST) { shuffleButton = TextUnderButton.create(requireContext(), R.drawable.ic_shuffle, buttonSize, 2, getString(R.string.lbl_shuffle_all), new View.OnClickListener() { @Override public void onClick(View v) { @@ -1060,7 +1056,7 @@ public void onClick(View v) { if (versions != null ) { addVersionsMenu(v); } else { - versions = mBaseItem.getMediaSources(); + versions = new ArrayList(mBaseItem.getMediaSources()); addVersionsMenu(v); } } @@ -1068,7 +1064,7 @@ public void onClick(View v) { mDetailsOverviewRow.addAction(mVersionsButton); } - if (TrailerUtils.hasPlayableTrailers(requireContext(), ModelCompat.asSdk(mBaseItem))) { + if (TrailerUtils.hasPlayableTrailers(requireContext(), mBaseItem)) { trailerButton = TextUnderButton.create(requireContext(), R.drawable.ic_trailer, buttonSize, 0, getString(R.string.lbl_play_trailers), new View.OnClickListener() { @Override public void onClick(View v) { @@ -1080,7 +1076,7 @@ public void onClick(View v) { } if (mProgramInfo != null && Utils.canManageRecordings(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue())) { - if (TimeUtils.convertToLocalDate(mBaseItem.getEndDate()).getTime() > Instant.now().toEpochMilli()) { + if (mBaseItem.getEndDate().isAfter(LocalDateTime.now())) { //Record button mRecordButton = TextUnderButton.create(requireContext(), R.drawable.ic_record, buttonSize, 4, getString(R.string.lbl_record), new View.OnClickListener() { @Override @@ -1098,9 +1094,9 @@ public void onResponse() { if (!getActive()) return; // we have to re-retrieve the program to get the timer id - apiClient.getValue().GetLiveTvProgramAsync(mProgramInfo.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { + apiClient.getValue().GetLiveTvProgramAsync(mProgramInfo.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { @Override - public void onResponse(BaseItemDto response) { + public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { if (!getActive()) return; mProgramInfo = ModelCompat.asSdk(response); @@ -1172,9 +1168,9 @@ public void onResponse() { if (!getActive()) return; // we have to re-retrieve the program to get the timer id - apiClient.getValue().GetLiveTvProgramAsync(mProgramInfo.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { + apiClient.getValue().GetLiveTvProgramAsync(mProgramInfo.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { @Override - public void onResponse(BaseItemDto response) { + public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { if (!getActive()) return; mProgramInfo = ModelCompat.asSdk(response); @@ -1253,9 +1249,9 @@ public void onClick(View v) { } } - UserItemDataDto userData = mBaseItem.getUserData(); + org.jellyfin.sdk.model.api.UserItemDataDto userData = mBaseItem.getUserData(); if (userData != null && mProgramInfo == null) { - if (ModelCompat.asSdk(mBaseItem).getType() != BaseItemKind.MUSIC_ARTIST && ModelCompat.asSdk(mBaseItem).getType() != BaseItemKind.PERSON) { + if (mBaseItem.getType() != BaseItemKind.MUSIC_ARTIST && mBaseItem.getType() != BaseItemKind.PERSON) { mWatchedToggleButton = TextUnderButton.create(requireContext(), R.drawable.ic_watch, buttonSize, 0, getString(R.string.lbl_watched), markWatchedListener); mWatchedToggleButton.setActivated(userData.getPlayed()); mDetailsOverviewRow.addAction(mWatchedToggleButton); @@ -1268,11 +1264,11 @@ public void onClick(final View v) { toggleFavorite(); } }); - favButton.setActivated(userData.getIsFavorite()); + favButton.setActivated(userData.isFavorite()); mDetailsOverviewRow.addAction(favButton); } - if (ModelCompat.asSdk(mBaseItem).getType() == BaseItemKind.EPISODE && mBaseItem.getSeriesId() != null) { + if (mBaseItem.getType() == BaseItemKind.EPISODE && mBaseItem.getSeriesId() != null) { //add the prev button first so it will be there in proper position - we'll show it later if needed mPrevButton = TextUnderButton.create(requireContext(), R.drawable.ic_previous_episode, buttonSize, 3, getString(R.string.lbl_previous_episode), new View.OnClickListener() { @Override @@ -1288,8 +1284,8 @@ public void onClick(View v) { //now go get our prev episode id EpisodeQuery adjacent = new EpisodeQuery(); adjacent.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - adjacent.setSeriesId(mBaseItem.getSeriesId()); - adjacent.setAdjacentTo(mBaseItem.getId()); + adjacent.setSeriesId(mBaseItem.getSeriesId().toString()); + adjacent.setAdjacentTo(mBaseItem.getId().toString()); apiClient.getValue().GetEpisodesAsync(adjacent, new LifecycleAwareResponse(getLifecycle()) { @Override public void onResponse(ItemsResult response) { @@ -1320,7 +1316,7 @@ public void onClick(View v) { if (userPreferences.getValue().get(UserPreferences.Companion.getMediaManagementEnabled())) { boolean deletableItem = false; UserDto currentUser = KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue(); - if (mBaseItem.getBaseItemType() == BaseItemType.Recording && currentUser.getPolicy().getEnableLiveTvManagement() && mBaseItem.getCanDelete()) + if (mBaseItem.getType() == BaseItemKind.RECORDING && currentUser.getPolicy().getEnableLiveTvManagement() && mBaseItem.getCanDelete()) deletableItem = true; else if (mBaseItem.getCanDelete()) deletableItem = true; @@ -1335,13 +1331,13 @@ public void onClick(View v) { } } - if (mSeriesTimerInfo != null && mBaseItem.getBaseItemType() == BaseItemType.SeriesTimer) { + if (mSeriesTimerInfo != null) { //Settings mDetailsOverviewRow.addAction(TextUnderButton.create(requireContext(), R.drawable.ic_settings, buttonSize, 0, getString(R.string.lbl_series_settings), new View.OnClickListener() { @Override public void onClick(View v) { //show recording options - showRecordingOptions(mSeriesTimerInfo.getId(), ModelCompat.asSdk(mBaseItem), true); + showRecordingOptions(mSeriesTimerInfo.getId(), mBaseItem, true); } })); @@ -1390,13 +1386,13 @@ public void onError(Exception ex) { moreButton = TextUnderButton.create(requireContext(), R.drawable.ic_more, buttonSize, 0, getString(R.string.lbl_other_options), new View.OnClickListener() { @Override public void onClick(View v) { - FullDetailsFragmentHelperKt.showDetailsMenu(FullDetailsFragment.this, v, ModelCompat.asSdk(mBaseItem)); + FullDetailsFragmentHelperKt.showDetailsMenu(FullDetailsFragment.this, v, mBaseItem); } }); moreButton.setVisibility(View.GONE); mDetailsOverviewRow.addAction(moreButton); - if (ModelCompat.asSdk(mBaseItem).getType() != BaseItemKind.EPISODE) showMoreButtonIfNeeded(); //Episodes check for previous and then call this above + if (mBaseItem.getType() != BaseItemKind.EPISODE) showMoreButtonIfNeeded(); //Episodes check for previous and then call this above } private void addVersionsMenu(View v) { @@ -1412,12 +1408,12 @@ private void addVersionsMenu(View v) { @Override public boolean onMenuItemClick(MenuItem menuItem) { mDetailsOverviewRow.setSelectedMediaSourceIndex(menuItem.getItemId()); - apiClient.getValue().GetItemAsync(versions.get(mDetailsOverviewRow.getSelectedMediaSourceIndex()).getId(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { + apiClient.getValue().GetItemAsync(versions.get(mDetailsOverviewRow.getSelectedMediaSourceIndex()).getId(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { @Override - public void onResponse(BaseItemDto response) { + public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { if (!getActive()) return; - mBaseItem = response; + mBaseItem = ModelCompat.asSdk(response); mDorPresenter.getViewHolder().setItem(mDetailsOverviewRow); if (mVersionsButton != null) { mVersionsButton.requestFocus(); @@ -1466,7 +1462,7 @@ private void showMoreButtonIfNeeded() { } RecordPopup mRecordPopup; - public void showRecordingOptions(String id, final org.jellyfin.sdk.model.api.BaseItemDto program, final boolean recordSeries) { + public void showRecordingOptions(String id, final BaseItemDto program, final boolean recordSeries) { if (mRecordPopup == null) { int width = Utils.convertDpToPixel(requireContext(), 600); Point size = new Point(); @@ -1489,9 +1485,9 @@ public void onResponse() { if (!getActive()) return; // we have to re-retrieve the program to get the timer id - apiClient.getValue().GetLiveTvProgramAsync(mProgramInfo.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { + apiClient.getValue().GetLiveTvProgramAsync(mProgramInfo.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { @Override - public void onResponse(BaseItemDto response) { + public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { setRecTimer(response.getTimerId()); } }); @@ -1538,8 +1534,8 @@ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, private View.OnClickListener markWatchedListener = new View.OnClickListener() { @Override public void onClick(final View v) { - final UserItemDataDto data = mBaseItem.getUserData(); - if (mBaseItem.getIsFolderItem()) { + final org.jellyfin.sdk.model.api.UserItemDataDto data = mBaseItem.getUserData(); + if (mBaseItem.isFolder()) { if (data.getPlayed()) markUnPlayed(); else @@ -1556,21 +1552,21 @@ public void onClick(final View v) { }; private void markPlayed() { - apiClient.getValue().MarkPlayedAsync(mBaseItem.getId(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), null, new LifecycleAwareResponse(getLifecycle()) { + apiClient.getValue().MarkPlayedAsync(mBaseItem.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), null, new LifecycleAwareResponse(getLifecycle()) { @Override public void onResponse(UserItemDataDto response) { - mBaseItem.setUserData(response); + mBaseItem = JavaCompat.copyWithUserData(mBaseItem, ModelCompat.asSdk(response)); mWatchedToggleButton.setActivated(true); //adjust resume - if (mResumeButton != null && !mBaseItem.getCanResume()) + if (mResumeButton != null && !JavaCompat.getCanResume(mBaseItem)) mResumeButton.setVisibility(View.GONE); //force lists to re-fetch dataRefreshService.getValue().setLastPlayback(Instant.now()); switch (mBaseItem.getType()) { - case "Movie": + case MOVIE: dataRefreshService.getValue().setLastMoviePlayback(Instant.now()); break; - case "Episode": + case EPISODE: dataRefreshService.getValue().setLastTvPlayback(Instant.now()); break; } @@ -1581,21 +1577,21 @@ public void onResponse(UserItemDataDto response) { } private void markUnPlayed() { - apiClient.getValue().MarkUnplayedAsync(mBaseItem.getId(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { + apiClient.getValue().MarkUnplayedAsync(mBaseItem.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { @Override public void onResponse(UserItemDataDto response) { - mBaseItem.setUserData(response); + mBaseItem = JavaCompat.copyWithUserData(mBaseItem, ModelCompat.asSdk(response)); mWatchedToggleButton.setActivated(false); //adjust resume - if (mResumeButton != null && !mBaseItem.getCanResume()) + if (mResumeButton != null && !JavaCompat.getCanResume(mBaseItem)) mResumeButton.setVisibility(View.GONE); //force lists to re-fetch dataRefreshService.getValue().setLastPlayback(Instant.now()); switch (mBaseItem.getType()) { - case "Movie": + case MOVIE: dataRefreshService.getValue().setLastMoviePlayback(Instant.now()); break; - case "Episode": + case EPISODE: dataRefreshService.getValue().setLastTvPlayback(Instant.now()); break; } @@ -1610,16 +1606,16 @@ void shufflePlay() { } protected void play(final BaseItemDto item, final int pos, final boolean shuffle) { - playbackHelper.getValue().getItemsToPlay(getContext(), ModelCompat.asSdk(item), pos == 0 && ModelCompat.asSdk(item).getType() == BaseItemKind.MOVIE, shuffle, new LifecycleAwareResponse>(getLifecycle()) { + playbackHelper.getValue().getItemsToPlay(getContext(), item, pos == 0 && item.getType() == BaseItemKind.MOVIE, shuffle, new LifecycleAwareResponse>(getLifecycle()) { @Override - public void onResponse(List response) { + public void onResponse(List response) { if (!getActive()) return; - if (ModelCompat.asSdk(item).getType() == BaseItemKind.MUSIC_ARTIST) { + if (item.getType() == BaseItemKind.MUSIC_ARTIST) { mediaManager.getValue().playNow(requireContext(), response, 0, shuffle); } else { videoQueueManager.getValue().setCurrentVideoQueue(response); - Destination destination = KoinJavaComponent.get(PlaybackLauncher.class).getPlaybackDestination(ModelCompat.asSdk(item).getType(), pos); + Destination destination = KoinJavaComponent.get(PlaybackLauncher.class).getPlaybackDestination(item.getType(), pos); navigationRepository.getValue().navigate(destination); } } @@ -1627,11 +1623,10 @@ public void onResponse(List response) { } - protected void play(final BaseItemDto[] items, final int pos, final boolean shuffle) { - List itemsToPlay = Arrays.asList(items); - if (shuffle) Collections.shuffle(itemsToPlay); - videoQueueManager.getValue().setCurrentVideoQueue(JavaCompat.mapBaseItemCollection(itemsToPlay)); - Destination destination = KoinJavaComponent.get(PlaybackLauncher.class).getPlaybackDestination(ModelCompat.asSdk(items[0]).getType(), pos); + protected void play(final List items, final int pos, final boolean shuffle) { + if (shuffle) Collections.shuffle(items); + videoQueueManager.getValue().setCurrentVideoQueue(items); + Destination destination = KoinJavaComponent.get(PlaybackLauncher.class).getPlaybackDestination(items.get(0).getType(), pos); navigationRepository.getValue().navigate(destination); } } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt index 0901fb3631..b7ca7990a0 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt @@ -14,14 +14,17 @@ import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer import org.jellyfin.androidtv.ui.navigation.Destinations import org.jellyfin.androidtv.ui.navigation.NavigationRepository import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse +import org.jellyfin.androidtv.util.apiclient.getSeriesOverview import org.jellyfin.androidtv.util.popupMenu -import org.jellyfin.androidtv.util.sdk.compat.asSdk import org.jellyfin.androidtv.util.showIfNotEmpty import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.exception.ApiClientException import org.jellyfin.sdk.api.client.extensions.libraryApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.MediaType +import org.jellyfin.sdk.model.api.SeriesTimerInfoDto +import org.jellyfin.sdk.model.serializer.toUUID import timber.log.Timber fun FullDetailsFragment.deleteItem( @@ -98,12 +101,11 @@ fun FullDetailsFragment.showPlayWithMenu( item(getString(R.string.play_with_external_app)) { systemPreferences.value[SystemPreferences.chosenPlayer] = PreferredVideoPlayer.EXTERNAL - val baseItem = mBaseItem.asSdk() val itemsCallback = object : LifecycleAwareResponse>(lifecycle) { override fun onResponse(response: List) { if (!active) return - if (baseItem.type == BaseItemKind.MUSIC_ARTIST) { + if (mBaseItem.type == BaseItemKind.MUSIC_ARTIST) { mediaManager.value.playNow(requireContext(), response, 0, false) } else { videoQueueManager.value.setCurrentVideoQueue(response) @@ -112,6 +114,15 @@ fun FullDetailsFragment.showPlayWithMenu( } } - playbackHelper.value.getItemsToPlay(requireContext(), baseItem, false, shuffle, itemsCallback) + playbackHelper.value.getItemsToPlay(requireContext(), mBaseItem, false, shuffle, itemsCallback) } }.show() + +fun FullDetailsFragment.createFakeSeriesTimerBaseItemDto(timer: SeriesTimerInfoDto) = BaseItemDto( + id = requireNotNull(timer.id).toUUID(), + type = BaseItemKind.FOLDER, + mediaType = MediaType.UNKNOWN, + seriesTimerId = timer.id, + name = timer.name, + overview = timer.getSeriesOverview(requireContext()), +) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/itemhandling/ItemRowAdapter.java b/app/src/main/java/org/jellyfin/androidtv/ui/itemhandling/ItemRowAdapter.java index e1f398f3e9..02b3d43e28 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/itemhandling/ItemRowAdapter.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/itemhandling/ItemRowAdapter.java @@ -257,11 +257,11 @@ public ItemRowAdapter(Context context, LatestItemsQuery query, boolean preferPar staticHeight = true; } - public ItemRowAdapter(Context context, BaseItemPerson[] people, Presenter presenter, MutableObjectAdapter parent) { + public ItemRowAdapter(List people, Context context, Presenter presenter, MutableObjectAdapter parent) { super(presenter); this.context = context; mParent = parent; - mPersons = people; + mPersons = people.toArray(new BaseItemPerson[people.size()]); staticHeight = true; queryType = QueryType.StaticPeople; } diff --git a/app/src/main/java/org/jellyfin/androidtv/util/sdk/compat/JavaCompat.kt b/app/src/main/java/org/jellyfin/androidtv/util/sdk/compat/JavaCompat.kt index 3b19cc6557..50894dd14f 100644 --- a/app/src/main/java/org/jellyfin/androidtv/util/sdk/compat/JavaCompat.kt +++ b/app/src/main/java/org/jellyfin/androidtv/util/sdk/compat/JavaCompat.kt @@ -4,6 +4,9 @@ package org.jellyfin.androidtv.util.sdk.compat import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.MediaSourceInfo +import org.jellyfin.sdk.model.api.UserItemDataDto +import java.time.LocalDateTime +import java.util.UUID import org.jellyfin.apiclient.model.dto.BaseItemDto as LegacyBaseItemdto fun BaseItemDto.copyWithDisplayPreferencesId( @@ -12,16 +15,49 @@ fun BaseItemDto.copyWithDisplayPreferencesId( displayPreferencesId = displayPreferencesId, ) +fun BaseItemDto.copyWithDates( + premiereDate: LocalDateTime?, + endDate: LocalDateTime?, + officialRating: String?, + runTimeTicks: Long?, +) = copy( + premiereDate = premiereDate, + endDate = endDate, + officialRating = officialRating, + runTimeTicks = runTimeTicks, +) + fun BaseItemDto.copyWithTimerId( seriesTimerId: String?, ) = copy( seriesTimerId = seriesTimerId, ) +fun BaseItemDto.copyWithOverview( + overview: String?, +) = copy( + overview = overview, +) + +fun BaseItemDto.copyWithParentId( + parentId: UUID?, +) = copy( + parentId = parentId, +) + +fun BaseItemDto.copyWithUserData( + userData: UserItemDataDto?, +) = copy( + userData = userData, +) + fun BaseItemDto.getResumePositionTicks() = userData?.playbackPositionTicks ?: 0 fun Collection.mapBaseItemCollection(): List = map { it.asSdk() } +fun Array.mapBaseItemArray(): List = map { it.asSdk() } fun MediaSourceInfo.getVideoStream() = mediaStreams?.firstOrNull { it.type == org.jellyfin.sdk.model.api.MediaStreamType.VIDEO } + +val BaseItemDto.canResume get() = (userData?.playbackPositionTicks ?: 0) > 0 From 2e28154dd4e7bd2e2ccbac650e088660636cc900 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 19 May 2024 17:53:29 +0200 Subject: [PATCH 2/2] Use SDK for API calls in FullDetailsFragment --- .../ui/itemdetail/FullDetailsFragment.java | 342 +++++------------- .../itemdetail/FullDetailsFragmentHelper.kt | 168 ++++++++- 2 files changed, 250 insertions(+), 260 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java index 06b36a89f9..5e45936eb3 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragment.java @@ -3,9 +3,7 @@ import static org.koin.java.KoinJavaComponent.inject; import android.app.AlertDialog; -import android.content.ActivityNotFoundException; import android.content.DialogInterface; -import android.content.Intent; import android.graphics.Point; import android.os.AsyncTask; import android.os.Bundle; @@ -87,13 +85,10 @@ import org.jellyfin.androidtv.util.sdk.compat.JavaCompat; import org.jellyfin.androidtv.util.sdk.compat.ModelCompat; import org.jellyfin.apiclient.interaction.ApiClient; -import org.jellyfin.apiclient.model.dto.UserItemDataDto; import org.jellyfin.apiclient.model.livetv.ChannelInfoDto; import org.jellyfin.apiclient.model.livetv.TimerQuery; -import org.jellyfin.apiclient.model.querying.EpisodeQuery; import org.jellyfin.apiclient.model.querying.ItemFields; import org.jellyfin.apiclient.model.querying.ItemQuery; -import org.jellyfin.apiclient.model.querying.ItemsResult; import org.jellyfin.apiclient.model.querying.NextUpQuery; import org.jellyfin.apiclient.model.querying.SeasonQuery; import org.jellyfin.apiclient.model.querying.SimilarItemsQuery; @@ -129,13 +124,13 @@ public class FullDetailsFragment extends Fragment implements RecordingIndicatorV private int BUTTON_SIZE; - private TextUnderButton mResumeButton; + TextUnderButton mResumeButton; private TextUnderButton mVersionsButton; - private TextUnderButton mPrevButton; + TextUnderButton mPrevButton; private TextUnderButton mRecordButton; private TextUnderButton mRecSeriesButton; private TextUnderButton mSeriesSettingsButton; - private TextUnderButton mWatchedToggleButton; + TextUnderButton mWatchedToggleButton; private DisplayMetrics mMetrics; @@ -145,7 +140,7 @@ public class FullDetailsFragment extends Fragment implements RecordingIndicatorV protected UUID mChannelId; protected BaseRowItem mCurrentItem; private Instant mLastUpdated; - private UUID mPrevItemId; + public UUID mPrevItemId; private RowsSupportFragment mRowsFragment; private MutableObjectAdapter mRowsAdapter; @@ -198,7 +193,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c mChannelId = Utils.uuidOrNull(getArguments().getString("ChannelId")); String programJson = getArguments().getString("ProgramInfo"); if (programJson != null) { - mProgramInfo =Json.Default.decodeFromString(BaseItemDto.Companion.serializer(), programJson); + mProgramInfo = Json.Default.decodeFromString(BaseItemDto.Companion.serializer(), programJson); } String timerJson = getArguments().getString("SeriesTimer"); if (timerJson != null) { @@ -225,7 +220,8 @@ public void onResponse(org.jellyfin.apiclient.model.livetv.SeriesTimerInfoDto re new Handler().postDelayed(new Runnable() { @Override public void run() { - if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) return; + if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) + return; addAdditionalRows(mRowsAdapter); @@ -240,7 +236,7 @@ public void run() { return binding.getRoot(); } - private int getResumePreroll() { + int getResumePreroll() { try { return Integer.parseInt(KoinJavaComponent.get(UserPreferences.class).get(UserPreferences.Companion.getResumeSubtractDuration())) * 1000; } catch (Exception e) { @@ -277,28 +273,26 @@ public void run() { dataRefreshService.getValue().setLastPlayedItem(null); //blank this out so a detail screen we back up to doesn't also do this } else { Timber.d("Updating info after playback"); - apiClient.getValue().GetItemAsync(mBaseItem.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { - @Override - public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { - if (!getActive()) return; - - mBaseItem = ModelCompat.asSdk(response); - if (mResumeButton != null) { - boolean resumeVisible = (mBaseItem.getType() == BaseItemKind.SERIES && !mBaseItem.getUserData().getPlayed()) || response.getCanResume(); - mResumeButton.setVisibility(resumeVisible ? View.VISIBLE : View.GONE); - if (response.getCanResume()) { - mResumeButton.setLabel(getString(R.string.lbl_resume_from, TimeUtils.formatMillis((response.getUserData().getPlaybackPositionTicks()/10000) - getResumePreroll()))); - } - if (resumeVisible) { - mResumeButton.requestFocus(); - } else if (playButton != null && ViewKt.isVisible(playButton)) { - playButton.requestFocus(); - } - showMoreButtonIfNeeded(); + FullDetailsFragmentHelperKt.getItem(FullDetailsFragment.this, mBaseItem.getId(), item -> { + if (item == null) return null; + + mBaseItem = item; + if (mResumeButton != null) { + boolean resumeVisible = (mBaseItem.getType() == BaseItemKind.SERIES && !mBaseItem.getUserData().getPlayed()) || JavaCompat.getCanResume(mBaseItem); + mResumeButton.setVisibility(resumeVisible ? View.VISIBLE : View.GONE); + if (JavaCompat.getCanResume(mBaseItem)) { + mResumeButton.setLabel(getString(R.string.lbl_resume_from, TimeUtils.formatMillis((mBaseItem.getUserData().getPlaybackPositionTicks() / 10000) - getResumePreroll()))); } - updateWatched(); - mLastUpdated = Instant.now(); + if (resumeVisible) { + mResumeButton.requestFocus(); + } else if (playButton != null && ViewKt.isVisible(playButton)) { + playButton.requestFocus(); + } + showMoreButtonIfNeeded(); } + updateWatched(); + mLastUpdated = Instant.now(); + return null; }); } } @@ -327,7 +321,7 @@ public boolean onKey(View v, int keyCode, KeyEvent event) { } else if ((keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) && BaseItemExtensionsKt.canPlay(mBaseItem)) { //default play action Long pos = mBaseItem.getUserData().getPlaybackPositionTicks() / 10000; - play(mBaseItem, pos.intValue() , false); + play(mBaseItem, pos.intValue(), false); return true; } @@ -393,34 +387,28 @@ public void onResponse(ChannelInfoDto response) { mProgramInfo = ModelCompat.asSdk(response.getCurrentProgram()); mItemId = mProgramInfo.getId(); - apiClient.getValue().GetItemAsync(mItemId.toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { - @Override - public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { - if (!getActive()) return; - - setBaseItem(ModelCompat.asSdk(response)); + FullDetailsFragmentHelperKt.getItem(FullDetailsFragment.this, mItemId, item -> { + if (item != null) { + setBaseItem(item); + } else { + // Failed to load item + navigationRepository.getValue().goBack(); } + return null; }); } }); } else if (mSeriesTimerInfo != null) { setBaseItem(FullDetailsFragmentHelperKt.createFakeSeriesTimerBaseItemDto(this, mSeriesTimerInfo)); } else { - apiClient.getValue().GetItemAsync(id.toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { - @Override - public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { - if (!getActive()) return; - - setBaseItem(ModelCompat.asSdk(response)); - } - - @Override - public void onError(@Nullable Exception exception) { - Timber.w(exception, "Failed to load item, trying to navigate back."); - super.onError(exception); - + FullDetailsFragmentHelperKt.getItem(FullDetailsFragment.this, id, item -> { + if (item != null) { + setBaseItem(item); + } else { + // Failed to load item navigationRepository.getValue().goBack(); } + return null; }); } @@ -439,7 +427,8 @@ public void setRecTimer(String id) { public void setRecSeriesTimer(String id) { if (mProgramInfo != null) mProgramInfo = JavaCompat.copyWithTimerId(mProgramInfo, id); if (mRecSeriesButton != null) mRecSeriesButton.setActivated(id != null); - if (mSeriesSettingsButton != null) mSeriesSettingsButton.setVisibility(id == null ? View.GONE : View.VISIBLE); + if (mSeriesSettingsButton != null) + mSeriesSettingsButton.setVisibility(id == null ? View.GONE : View.VISIBLE); } @@ -555,7 +544,8 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { if (mSeriesTimerInfo != null) { TimerQuery scheduled = new TimerQuery(); scheduled.setSeriesTimerId(mSeriesTimerInfo.getId()); - TvManager.getScheduleRowsAsync(requireContext(), scheduled, new CardPresenter(true), adapter, new LifecycleAwareResponse(getLifecycle()) { }); + TvManager.getScheduleRowsAsync(requireContext(), scheduled, new CardPresenter(true), adapter, new LifecycleAwareResponse(getLifecycle()) { + }); return; } @@ -593,7 +583,7 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { //Similar SimilarItemsQuery similar = new SimilarItemsQuery(); - similar.setFields(new ItemFields[] { + similar.setFields(new ItemFields[]{ ItemFields.PrimaryImageAspectRatio, ItemFields.ChildCount }); @@ -616,7 +606,7 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { //Similar SimilarItemsQuery similarTrailer = new SimilarItemsQuery(); - similarTrailer.setFields(new ItemFields[] { + similarTrailer.setFields(new ItemFields[]{ ItemFields.PrimaryImageAspectRatio, ItemFields.ChildCount }); @@ -636,10 +626,10 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { ItemFields.ChildCount }); personMovies.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - personMovies.setPersonIds(new String[] {mBaseItem.getId().toString()}); + personMovies.setPersonIds(new String[]{mBaseItem.getId().toString()}); personMovies.setRecursive(true); - personMovies.setIncludeItemTypes(new String[] {"Movie"}); - personMovies.setSortBy(new String[] {ItemSortBy.SORT_NAME.getSerialName()}); + personMovies.setIncludeItemTypes(new String[]{"Movie"}); + personMovies.setSortBy(new String[]{ItemSortBy.SORT_NAME.getSerialName()}); ItemRowAdapter personMoviesAdapter = new ItemRowAdapter(requireContext(), personMovies, 100, false, new CardPresenter(), adapter); addItemRow(adapter, personMoviesAdapter, 0, getString(R.string.lbl_movies)); @@ -650,10 +640,10 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { ItemFields.ChildCount }); personSeries.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - personSeries.setPersonIds(new String[] {mBaseItem.getId().toString()}); + personSeries.setPersonIds(new String[]{mBaseItem.getId().toString()}); personSeries.setRecursive(true); - personSeries.setIncludeItemTypes(new String[] {"Series"}); - personSeries.setSortBy(new String[] {ItemSortBy.SORT_NAME.getSerialName()}); + personSeries.setIncludeItemTypes(new String[]{"Series"}); + personSeries.setSortBy(new String[]{ItemSortBy.SORT_NAME.getSerialName()}); ItemRowAdapter personSeriesAdapter = new ItemRowAdapter(requireContext(), personSeries, 100, false, new CardPresenter(), adapter); addItemRow(adapter, personSeriesAdapter, 1, getString(R.string.lbl_tv_series)); @@ -664,10 +654,10 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { ItemFields.ChildCount }); personEpisodes.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - personEpisodes.setPersonIds(new String[] {mBaseItem.getId().toString()}); + personEpisodes.setPersonIds(new String[]{mBaseItem.getId().toString()}); personEpisodes.setRecursive(true); - personEpisodes.setIncludeItemTypes(new String[] {"Episode"}); - personEpisodes.setSortBy(new String[] {ItemSortBy.SERIES_SORT_NAME.getSerialName(), ItemSortBy.SORT_NAME.getSerialName()}); + personEpisodes.setIncludeItemTypes(new String[]{"Episode"}); + personEpisodes.setSortBy(new String[]{ItemSortBy.SERIES_SORT_NAME.getSerialName(), ItemSortBy.SORT_NAME.getSerialName()}); ItemRowAdapter personEpisodesAdapter = new ItemRowAdapter(requireContext(), personEpisodes, 100, false, new CardPresenter(), adapter); addItemRow(adapter, personEpisodesAdapter, 2, getString(R.string.lbl_episodes)); @@ -701,7 +691,7 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { SeasonQuery seasons = new SeasonQuery(); seasons.setSeriesId(mBaseItem.getId().toString()); seasons.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - seasons.setFields(new ItemFields[] { + seasons.setFields(new ItemFields[]{ ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId, ItemFields.ChildCount @@ -750,7 +740,7 @@ protected void addAdditionalRows(MutableObjectAdapter adapter) { nextEpisodes.setIncludeItemTypes(new String[]{"Episode"}); nextEpisodes.setStartIndex(mBaseItem.getIndexNumber()); // query index is zero-based but episode no is not nextEpisodes.setLimit(20); - ItemRowAdapter nextAdapter = new ItemRowAdapter(requireContext(), nextEpisodes, 0 , false, true, new CardPresenter(true, 120), adapter); + ItemRowAdapter nextAdapter = new ItemRowAdapter(requireContext(), nextEpisodes, 0, false, true, new CardPresenter(true, 120), adapter); addItemRow(adapter, nextAdapter, 5, getString(R.string.lbl_next_episode)); } @@ -782,7 +772,7 @@ private void addInfoRows(MutableObjectAdapter adapter) { if (KoinJavaComponent.get(UserPreferences.class).get(UserPreferences.Companion.getDebuggingEnabled()) && mBaseItem.getMediaSources() != null) { for (MediaSourceInfo ms : mBaseItem.getMediaSources()) { if (ms.getMediaStreams() != null && !ms.getMediaStreams().isEmpty()) { - HeaderItem header = new HeaderItem("Media Details"+(ms.getContainer() != null ? " (" +ms.getContainer()+")" : "")); + HeaderItem header = new HeaderItem("Media Details" + (ms.getContainer() != null ? " (" + ms.getContainer() + ")" : "")); ArrayObjectAdapter infoAdapter = new ArrayObjectAdapter(new InfoCardPresenter()); for (MediaStream stream : ms.getMediaStreams()) { infoAdapter.add(stream); @@ -805,40 +795,6 @@ public void setTitle(String title) { mDorPresenter.getViewHolder().setTitle(title); } - void playTrailers() { - // External trailer - if (mBaseItem.getLocalTrailerCount() == null || mBaseItem.getLocalTrailerCount() < 1) { - Intent intent = TrailerUtils.getExternalTrailerIntent(requireContext(), mBaseItem); - - try { - startActivity(intent); - } catch (ActivityNotFoundException exception) { - Timber.w(exception, "Unable to open external trailer"); - Utils.showToast(requireContext(), getString(R.string.no_player_message)); - } - - return; - } - - // Local trailer - apiClient.getValue().GetLocalTrailersAsync(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), mBaseItem.getId().toString(), new LifecycleAwareResponse(getLifecycle()) { - @Override - public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto[] response) { - if (!getActive()) return; - - play(JavaCompat.mapBaseItemArray(response), 0, false); - } - - @Override - public void onError(Exception exception) { - if (!getActive()) return; - - Timber.e(exception, "Error retrieving trailers for playback"); - Utils.showToast(requireContext(), R.string.msg_video_playback_error); - } - }); - } - private String getRunTime() { Long runtime = Utils.getSafeValue(mBaseItem.getRunTimeTicks(), mBaseItem.getRunTimeTicks()); return runtime != null && runtime > 0 ? String.format("%d%s", (int) Math.ceil((double) runtime / 600000000), getString(R.string.lbl_min)) : ""; @@ -877,20 +833,6 @@ public void onResponse(List response) { } } - void toggleFavorite() { - org.jellyfin.sdk.model.api.UserItemDataDto data = mBaseItem.getUserData(); - apiClient.getValue().UpdateFavoriteStatusAsync(mBaseItem.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), !data.isFavorite(), new LifecycleAwareResponse(getLifecycle()) { - @Override - public void onResponse(UserItemDataDto response) { - if (!getActive()) return; - - mBaseItem = JavaCompat.copyWithUserData(mBaseItem, ModelCompat.asSdk(response)); - favButton.setActivated(response.getIsFavorite()); - dataRefreshService.getValue().setLastFavoriteUpdate(Instant.now()); - } - }); - } - void gotoSeries() { navigationRepository.getValue().navigate(Destinations.INSTANCE.itemDetails(mBaseItem.getSeriesId())); } @@ -930,44 +872,14 @@ private void addButtons(int buttonSize) { } else { long startPos = 0; if (JavaCompat.getCanResume(mBaseItem)) { - startPos = (mBaseItem.getUserData().getPlaybackPositionTicks()/10000) - getResumePreroll(); + startPos = (mBaseItem.getUserData().getPlaybackPositionTicks() / 10000) - getResumePreroll(); } buttonLabel = getString(R.string.lbl_resume_from, TimeUtils.formatMillis(startPos)); } mResumeButton = TextUnderButton.create(requireContext(), R.drawable.ic_resume, buttonSize, 2, buttonLabel, new View.OnClickListener() { @Override public void onClick(View v) { - if (baseItem.getType() == BaseItemKind.SERIES) { - //play next up - NextUpQuery nextUpQuery = new NextUpQuery(); - nextUpQuery.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - nextUpQuery.setSeriesId(mBaseItem.getId().toString()); - apiClient.getValue().GetNextUpEpisodesAsync(nextUpQuery, new LifecycleAwareResponse(getLifecycle()) { - @Override - public void onResponse(ItemsResult response) { - if (!getActive()) return; - - if (response.getItems().length > 0) { - play(ModelCompat.asSdk(response.getItems()[0]), 0 , false); - } else { - Utils.showToast(requireContext(), "Unable to find next up episode"); - } - } - - @Override - public void onError(Exception exception) { - if (!getActive()) return; - - Timber.e(exception, "Error playing next up episode"); - Utils.showToast(requireContext(), getString(R.string.msg_video_playback_error)); - } - }); - } else { - //resume - Long pos = mBaseItem.getUserData().getPlaybackPositionTicks() / 10000; - play(mBaseItem, pos.intValue() - getResumePreroll(), false); - - } + FullDetailsFragmentHelperKt.resumePlayback(FullDetailsFragment.this); } }); @@ -1053,7 +965,7 @@ public void onClick(View v) { mVersionsButton = TextUnderButton.create(requireContext(), R.drawable.ic_guide, buttonSize, 0, getString(R.string.select_version), new View.OnClickListener() { @Override public void onClick(View v) { - if (versions != null ) { + if (versions != null) { addVersionsMenu(v); } else { versions = new ArrayList(mBaseItem.getMediaSources()); @@ -1068,7 +980,7 @@ public void onClick(View v) { trailerButton = TextUnderButton.create(requireContext(), R.drawable.ic_trailer, buttonSize, 0, getString(R.string.lbl_play_trailers), new View.OnClickListener() { @Override public void onClick(View v) { - playTrailers(); + FullDetailsFragmentHelperKt.playTrailers(FullDetailsFragment.this); } }); @@ -1154,7 +1066,7 @@ public void onError(Exception ex) { } if (mProgramInfo.isSeries() != null && mProgramInfo.isSeries()) { - mRecSeriesButton= TextUnderButton.create(requireContext(), R.drawable.ic_record_series, buttonSize, 4, getString(R.string.lbl_record_series), new View.OnClickListener() { + mRecSeriesButton = TextUnderButton.create(requireContext(), R.drawable.ic_record_series, buttonSize, 4, getString(R.string.lbl_record_series), new View.OnClickListener() { @Override public void onClick(View v) { if (mProgramInfo.getSeriesTimerId() == null) { @@ -1261,7 +1173,7 @@ public void onClick(View v) { favButton = TextUnderButton.create(requireContext(), R.drawable.ic_heart, buttonSize, 2, getString(R.string.lbl_favorite), new View.OnClickListener() { @Override public void onClick(final View v) { - toggleFavorite(); + FullDetailsFragmentHelperKt.toggleFavorite(FullDetailsFragment.this); } }); favButton.setActivated(userData.isFavorite()); @@ -1282,27 +1194,7 @@ public void onClick(View v) { mDetailsOverviewRow.addAction(mPrevButton); //now go get our prev episode id - EpisodeQuery adjacent = new EpisodeQuery(); - adjacent.setUserId(KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString()); - adjacent.setSeriesId(mBaseItem.getSeriesId().toString()); - adjacent.setAdjacentTo(mBaseItem.getId().toString()); - apiClient.getValue().GetEpisodesAsync(adjacent, new LifecycleAwareResponse(getLifecycle()) { - @Override - public void onResponse(ItemsResult response) { - if (!getActive()) return; - - if (response.getTotalRecordCount() > 0) { - //Just look at first item - if it isn't us, then it is the prev episode - if (!mBaseItem.getId().equals(response.getItems()[0].getId())) { - mPrevItemId = UUIDSerializerKt.toUUID(response.getItems()[0].getId()); - mPrevButton.setVisibility(View.VISIBLE); - } else { - mPrevButton.setVisibility(View.GONE); - } - } - showMoreButtonIfNeeded(); - } - }); + FullDetailsFragmentHelperKt.populatePreviousButton(FullDetailsFragment.this); goToSeriesButton = TextUnderButton.create(requireContext(), R.drawable.ic_tv, buttonSize, 0, getString(R.string.lbl_goto_series), new View.OnClickListener() { @Override @@ -1392,33 +1284,30 @@ public void onClick(View v) { moreButton.setVisibility(View.GONE); mDetailsOverviewRow.addAction(moreButton); - if (mBaseItem.getType() != BaseItemKind.EPISODE) showMoreButtonIfNeeded(); //Episodes check for previous and then call this above + if (mBaseItem.getType() != BaseItemKind.EPISODE) + showMoreButtonIfNeeded(); //Episodes check for previous and then call this above } private void addVersionsMenu(View v) { PopupMenu menu = new PopupMenu(requireContext(), v, Gravity.END); - for (int i = 0; i< versions.size(); i++) { + for (int i = 0; i < versions.size(); i++) { MenuItem item = menu.getMenu().add(Menu.NONE, i, Menu.NONE, versions.get(i).getName()); item.setChecked(i == mDetailsOverviewRow.getSelectedMediaSourceIndex()); } - menu.getMenu().setGroupCheckable(0,true,false); + menu.getMenu().setGroupCheckable(0, true, false); menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem menuItem) { mDetailsOverviewRow.setSelectedMediaSourceIndex(menuItem.getItemId()); - apiClient.getValue().GetItemAsync(versions.get(mDetailsOverviewRow.getSelectedMediaSourceIndex()).getId(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { - @Override - public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { - if (!getActive()) return; + FullDetailsFragmentHelperKt.getItem(FullDetailsFragment.this, UUIDSerializerKt.toUUID(versions.get(mDetailsOverviewRow.getSelectedMediaSourceIndex()).getId()), item -> { + if (item == null) return null; - mBaseItem = ModelCompat.asSdk(response); - mDorPresenter.getViewHolder().setItem(mDetailsOverviewRow); - if (mVersionsButton != null) { - mVersionsButton.requestFocus(); - } - } + mBaseItem = item; + mDorPresenter.getViewHolder().setItem(mDetailsOverviewRow); + if (mVersionsButton != null) mVersionsButton.requestFocus(); + return null; }); return true; } @@ -1427,9 +1316,9 @@ public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) { menu.show(); } - int collapsedOptions = 0 ; + int collapsedOptions = 0; - private void showMoreButtonIfNeeded() { + void showMoreButtonIfNeeded() { int visibleOptions = mDetailsOverviewRow.getVisibleActions(); List actionsList = new ArrayList<>(); @@ -1462,12 +1351,13 @@ private void showMoreButtonIfNeeded() { } RecordPopup mRecordPopup; + public void showRecordingOptions(String id, final BaseItemDto program, final boolean recordSeries) { if (mRecordPopup == null) { int width = Utils.convertDpToPixel(requireContext(), 600); Point size = new Point(); requireActivity().getWindowManager().getDefaultDisplay().getSize(size); - mRecordPopup = new RecordPopup(requireActivity(), getLifecycle(), mRowsFragment.getView(), (size.x/2) - (width/2), mRowsFragment.getView().getTop()+40, width); + mRecordPopup = new RecordPopup(requireActivity(), getLifecycle(), mRowsFragment.getView(), (size.x / 2) - (width / 2), mRowsFragment.getView().getTop() + 40, width); } apiClient.getValue().GetLiveTvSeriesTimerAsync(id, new LifecycleAwareResponse(getLifecycle()) { @Override @@ -1508,14 +1398,13 @@ public void onError(Exception ex) { } - private final class ItemViewClickedListener implements OnItemViewClickedListener { @Override public void onItemClicked(final Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (!(item instanceof BaseRowItem)) return; - itemLauncher.getValue().launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow)row).getAdapter(), ((BaseRowItem)item).getIndex(), requireContext()); + itemLauncher.getValue().launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), ((BaseRowItem) item).getIndex(), requireContext()); } } @@ -1526,7 +1415,7 @@ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, if (!(item instanceof BaseRowItem)) { mCurrentItem = null; } else { - mCurrentItem = (BaseRowItem)item; + mCurrentItem = (BaseRowItem) item; } } } @@ -1534,78 +1423,15 @@ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, private View.OnClickListener markWatchedListener = new View.OnClickListener() { @Override public void onClick(final View v) { - final org.jellyfin.sdk.model.api.UserItemDataDto data = mBaseItem.getUserData(); - if (mBaseItem.isFolder()) { - if (data.getPlayed()) - markUnPlayed(); - else - markPlayed(); - } else { - if (data.getPlayed()) { - markUnPlayed(); - } else { - markPlayed(); - } - } - + FullDetailsFragmentHelperKt.togglePlayed(FullDetailsFragment.this); } }; - private void markPlayed() { - apiClient.getValue().MarkPlayedAsync(mBaseItem.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), null, new LifecycleAwareResponse(getLifecycle()) { - @Override - public void onResponse(UserItemDataDto response) { - mBaseItem = JavaCompat.copyWithUserData(mBaseItem, ModelCompat.asSdk(response)); - mWatchedToggleButton.setActivated(true); - //adjust resume - if (mResumeButton != null && !JavaCompat.getCanResume(mBaseItem)) - mResumeButton.setVisibility(View.GONE); - //force lists to re-fetch - dataRefreshService.getValue().setLastPlayback(Instant.now()); - switch (mBaseItem.getType()) { - case MOVIE: - dataRefreshService.getValue().setLastMoviePlayback(Instant.now()); - break; - case EPISODE: - dataRefreshService.getValue().setLastTvPlayback(Instant.now()); - break; - } - showMoreButtonIfNeeded(); - } - }); - - } - - private void markUnPlayed() { - apiClient.getValue().MarkUnplayedAsync(mBaseItem.getId().toString(), KoinJavaComponent.get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new LifecycleAwareResponse(getLifecycle()) { - @Override - public void onResponse(UserItemDataDto response) { - mBaseItem = JavaCompat.copyWithUserData(mBaseItem, ModelCompat.asSdk(response)); - mWatchedToggleButton.setActivated(false); - //adjust resume - if (mResumeButton != null && !JavaCompat.getCanResume(mBaseItem)) - mResumeButton.setVisibility(View.GONE); - //force lists to re-fetch - dataRefreshService.getValue().setLastPlayback(Instant.now()); - switch (mBaseItem.getType()) { - case MOVIE: - dataRefreshService.getValue().setLastMoviePlayback(Instant.now()); - break; - case EPISODE: - dataRefreshService.getValue().setLastTvPlayback(Instant.now()); - break; - } - showMoreButtonIfNeeded(); - } - }); - - } - void shufflePlay() { play(mBaseItem, 0, true); } - protected void play(final BaseItemDto item, final int pos, final boolean shuffle) { + void play(final BaseItemDto item, final int pos, final boolean shuffle) { playbackHelper.getValue().getItemsToPlay(getContext(), item, pos == 0 && item.getType() == BaseItemKind.MOVIE, shuffle, new LifecycleAwareResponse>(getLifecycle()) { @Override public void onResponse(List response) { @@ -1623,7 +1449,7 @@ public void onResponse(List response) { } - protected void play(final List items, final int pos, final boolean shuffle) { + void play(final List items, final int pos, final boolean shuffle) { if (shuffle) Collections.shuffle(items); videoQueueManager.getValue().setCurrentVideoQueue(items); Destination destination = KoinJavaComponent.get(PlaybackLauncher.class).getPlaybackDestination(items.get(0).getType(), pos); diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt index b7ca7990a0..4c9adaed40 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/FullDetailsFragmentHelper.kt @@ -1,5 +1,6 @@ package org.jellyfin.androidtv.ui.itemdetail +import android.content.ActivityNotFoundException import android.view.View import android.widget.Toast import androidx.core.view.isVisible @@ -9,6 +10,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.androidtv.R import org.jellyfin.androidtv.data.model.DataRefreshService +import org.jellyfin.androidtv.data.repository.ItemMutationRepository import org.jellyfin.androidtv.preference.SystemPreferences import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer import org.jellyfin.androidtv.ui.navigation.Destinations @@ -16,16 +18,27 @@ import org.jellyfin.androidtv.ui.navigation.NavigationRepository import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse import org.jellyfin.androidtv.util.apiclient.getSeriesOverview import org.jellyfin.androidtv.util.popupMenu +import org.jellyfin.androidtv.util.sdk.TrailerUtils.getExternalTrailerIntent +import org.jellyfin.androidtv.util.sdk.compat.canResume +import org.jellyfin.androidtv.util.sdk.compat.copyWithUserData import org.jellyfin.androidtv.util.showIfNotEmpty import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.exception.ApiClientException import org.jellyfin.sdk.api.client.extensions.libraryApi +import org.jellyfin.sdk.api.client.extensions.tvShowsApi +import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.SeriesTimerInfoDto +import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.serializer.toUUID +import org.koin.android.ext.android.inject import timber.log.Timber +import java.time.Instant +import java.util.UUID +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds fun FullDetailsFragment.deleteItem( api: ApiClient, @@ -41,7 +54,11 @@ fun FullDetailsFragment.deleteItem( } } catch (error: ApiClientException) { Timber.e(error, "Failed to delete item ${item.name} (id=${item.id})") - Toast.makeText(context, getString(R.string.item_deletion_failed, item.name), Toast.LENGTH_LONG).show() + Toast.makeText( + context, + getString(R.string.item_deletion_failed, item.name), + Toast.LENGTH_LONG + ).show() return@launch } @@ -114,7 +131,13 @@ fun FullDetailsFragment.showPlayWithMenu( } } - playbackHelper.value.getItemsToPlay(requireContext(), mBaseItem, false, shuffle, itemsCallback) + playbackHelper.value.getItemsToPlay( + requireContext(), + mBaseItem, + false, + shuffle, + itemsCallback + ) } }.show() @@ -126,3 +149,144 @@ fun FullDetailsFragment.createFakeSeriesTimerBaseItemDto(timer: SeriesTimerInfoD name = timer.name, overview = timer.getSeriesOverview(requireContext()), ) + +fun FullDetailsFragment.toggleFavorite() { + val itemMutationRepository by inject() + val dataRefreshService by inject() + + lifecycleScope.launch { + val userData = itemMutationRepository.setFavorite( + item = mBaseItem.id, + favorite = !(mBaseItem.userData?.isFavorite ?: false) + ) + mBaseItem = mBaseItem.copyWithUserData(userData) + favButton.isActivated = userData.isFavorite + dataRefreshService.lastFavoriteUpdate = Instant.now() + } +} + +fun FullDetailsFragment.togglePlayed() { + val itemMutationRepository by inject() + val dataRefreshService by inject() + + lifecycleScope.launch { + val userData = itemMutationRepository.setPlayed( + item = mBaseItem.id, + played = !(mBaseItem.userData?.played ?: false) + ) + mBaseItem = mBaseItem.copyWithUserData(userData) + mWatchedToggleButton.isActivated = userData.played + + // Adjust resume + mResumeButton?.apply { + isVisible = mBaseItem.canResume + } + + // Force lists to re-fetch + dataRefreshService.lastPlayback = Instant.now() + when (mBaseItem.type) { + BaseItemKind.MOVIE -> dataRefreshService.lastMoviePlayback = Instant.now() + BaseItemKind.EPISODE -> dataRefreshService.lastTvPlayback = Instant.now() + else -> Unit + } + + showMoreButtonIfNeeded() + } +} + +fun FullDetailsFragment.playTrailers() { + val localTrailerCount = mBaseItem.localTrailerCount ?: 0 + + // External trailer + if (localTrailerCount < 1) try { + val intent = getExternalTrailerIntent(requireContext(), mBaseItem) + if (intent != null) startActivity(intent) + } catch (exception: ActivityNotFoundException) { + Timber.w(exception, "Unable to open external trailer") + Toast.makeText( + requireContext(), + getString(R.string.no_player_message), + Toast.LENGTH_LONG + ).show() + } else lifecycleScope.launch { + val api by inject() + + try { + val trailers by api.userLibraryApi.getLocalTrailers(mBaseItem.id) + play(trailers, 0, false) + } catch (exception: ApiClientException) { + Timber.e(exception, "Error retrieving trailers for playback") + Toast.makeText( + requireContext(), + getString(R.string.msg_video_playback_error), + Toast.LENGTH_LONG + ).show() + } + } +} + +fun FullDetailsFragment.getItem(id: UUID, callback: (item: BaseItemDto?) -> Unit) { + val api by inject() + + lifecycleScope.launch { + val response = try { + api.userLibraryApi.getItem(id) + } catch (err: ApiClientException) { + Timber.w(err, "Failed to get item $id") + null + } + + callback(response?.content) + } +} + +fun FullDetailsFragment.populatePreviousButton() { + if (mBaseItem.type != BaseItemKind.EPISODE) return + + val api by inject() + + lifecycleScope.launch { + val siblings by api.tvShowsApi.getEpisodes( + seriesId = requireNotNull(mBaseItem.seriesId), + adjacentTo = mBaseItem.id, + ) + + val previousItem = siblings.items + ?.filterNot { it.id == mBaseItem.id } + ?.firstOrNull() + ?.id + + mPrevItemId = previousItem + mPrevButton.isVisible = previousItem != null + + showMoreButtonIfNeeded() + } +} + +fun FullDetailsFragment.resumePlayback() { + if (mBaseItem.type != BaseItemKind.SERIES) { + val pos = (mBaseItem.userData?.playbackPositionTicks?.ticks + ?: Duration.ZERO) - resumePreroll.milliseconds + play(mBaseItem, pos.inWholeMilliseconds.toInt(), false) + return + } + + val api by inject() + + lifecycleScope.launch { + try { + val episodes by api.tvShowsApi.getNextUp( + seriesId = mBaseItem.id, + ) + + play(episodes.items, 0, false) + } catch (err: ApiClientException) { + Timber.w("Failed to get next up items") + Toast.makeText( + requireContext(), + getString(R.string.msg_video_playback_error), + Toast.LENGTH_LONG + ).show() + } + } +}