mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 09:35:42 +00:00
feat: implement intro submission feature and update to beta.14
This commit is contained in:
parent
28216b475f
commit
8d9fed3f7f
15 changed files with 828 additions and 121 deletions
10
ALPHA_BUILD_2_ANNOUNCEMENT.md
Normal file
10
ALPHA_BUILD_2_ANNOUNCEMENT.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Nuvio Alpha Build 2
|
||||
|
||||
This is the second alpha release of Nuvio!
|
||||
|
||||
## What's New
|
||||
- **Intro Submission:** You can now submit intro timestamps directly to IntroDB!
|
||||
- **Bug Fixes:** Various improvements and stability fixes.
|
||||
|
||||
## Installation
|
||||
Download the attached APK and install it on your Android device.
|
||||
|
|
@ -442,7 +442,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
|
||||
private void initializePlayerControl() {
|
||||
exoPlayerView.setPlayer(player);
|
||||
|
||||
|
||||
exoPlayerView.setControllerVisibilityListener(visibility -> {
|
||||
boolean isVisible = visibility == View.VISIBLE;
|
||||
eventEmitter.onControlsVisibilityChange.invoke(isVisible);
|
||||
|
|
@ -458,19 +458,19 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
private void updateControllerConfig() {
|
||||
if (exoPlayerView == null)
|
||||
return;
|
||||
|
||||
|
||||
exoPlayerView.setControllerShowTimeoutMs(5000);
|
||||
|
||||
|
||||
exoPlayerView.setControllerAutoShow(true);
|
||||
exoPlayerView.setControllerHideOnTouch(true);
|
||||
|
||||
|
||||
updateControllerVisibility();
|
||||
}
|
||||
|
||||
private void updateControllerVisibility() {
|
||||
if (exoPlayerView == null)
|
||||
return;
|
||||
|
||||
|
||||
exoPlayerView.setUseController(controls && !controlsConfig.getHideFullscreen());
|
||||
}
|
||||
|
||||
|
|
@ -520,7 +520,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
|
||||
/**
|
||||
* Update the layout
|
||||
*
|
||||
*
|
||||
* @param view view needs to update layout
|
||||
*
|
||||
* This is a workaround for the open bug in react-native: <a href=
|
||||
|
|
@ -758,7 +758,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
.forceEnableMediaCodecAsynchronousQueueing();
|
||||
|
||||
DefaultMediaSourceFactory mediaSourceFactory;
|
||||
|
||||
|
||||
if (isDaiRequest(source)) {
|
||||
mediaSourceFactory = createDaiMediaSourceFactory();
|
||||
} else {
|
||||
|
|
@ -872,7 +872,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
initializeDaiSource(runningSource);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (runningSource.getUri() == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1051,13 +1051,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
if (customMetadata != null) {
|
||||
mediaItemBuilder.setMediaMetadata(customMetadata);
|
||||
}
|
||||
|
||||
|
||||
// Add external subtitles to MediaItem
|
||||
List<MediaItem.SubtitleConfiguration> subtitleConfigurations = buildSubtitleConfigurations();
|
||||
if (subtitleConfigurations != null) {
|
||||
mediaItemBuilder.setSubtitleConfigurations(subtitleConfigurations);
|
||||
}
|
||||
|
||||
|
||||
if (source.getAdsProps() != null) {
|
||||
Uri adTagUrl = source.getAdsProps().getAdTagUrl();
|
||||
if (adTagUrl != null) {
|
||||
|
|
@ -1205,19 +1205,19 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
label += " (" + track.getLanguage() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder(
|
||||
track.getUri())
|
||||
.setId(trackId)
|
||||
.setMimeType(track.getType())
|
||||
.setLabel(label)
|
||||
.setRoleFlags(C.ROLE_FLAG_SUBTITLE);
|
||||
|
||||
|
||||
// Set language if available
|
||||
if (track.getLanguage() != null && !track.getLanguage().isEmpty()) {
|
||||
configBuilder.setLanguage(track.getLanguage());
|
||||
}
|
||||
|
||||
|
||||
// Set selection flags - make first track default if no specific track is
|
||||
// selected
|
||||
if (trackIndex == 0 && (textTrackType == null || "disabled".equals(textTrackType))) {
|
||||
|
|
@ -1225,10 +1225,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
} else {
|
||||
configBuilder.setSelectionFlags(0);
|
||||
}
|
||||
|
||||
|
||||
MediaItem.SubtitleConfiguration subtitleConfiguration = configBuilder.build();
|
||||
subtitleConfigurations.add(subtitleConfiguration);
|
||||
|
||||
|
||||
DebugLog.d(TAG,
|
||||
"Created subtitle configuration: " + trackId + " - " + label + " (" + track.getType() + ")");
|
||||
trackIndex++;
|
||||
|
|
@ -1549,7 +1549,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
}
|
||||
eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height,
|
||||
audioTracks, textTracks, videoTracks, trackId);
|
||||
|
||||
|
||||
updateSubtitleButtonVisibility();
|
||||
});
|
||||
return;
|
||||
|
|
@ -1589,19 +1589,19 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
|
||||
TrackGroup group = groups.get(groupIndex);
|
||||
Format format = group.getFormat(0);
|
||||
|
||||
|
||||
// Check if this specific group is the currently selected one
|
||||
boolean isSelected = false;
|
||||
if (selection != null && selection.getTrackGroup() == group) {
|
||||
isSelected = true;
|
||||
}
|
||||
|
||||
|
||||
Track audioTrack = exoplayerTrackToGenericTrack(format, groupIndex, selection, group);
|
||||
audioTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
||||
audioTrack.setSelected(isSelected);
|
||||
audioTracks.add(audioTrack);
|
||||
}
|
||||
|
||||
|
||||
return audioTracks;
|
||||
}
|
||||
|
||||
|
|
@ -1736,13 +1736,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
if (trackSelector == null) {
|
||||
return textTracks;
|
||||
}
|
||||
|
||||
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
int index = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (info == null || index == C.INDEX_UNSET) {
|
||||
return textTracks;
|
||||
}
|
||||
|
||||
|
||||
TrackSelectionArray selectionArray = player.getCurrentTrackSelections();
|
||||
TrackSelection selection = selectionArray.get(C.TRACK_TYPE_TEXT);
|
||||
TrackGroupArray groups = info.getTrackGroups(index);
|
||||
|
|
@ -1752,12 +1752,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
||||
Format format = group.getFormat(trackIndex);
|
||||
Track textTrack = exoplayerTrackToGenericTrack(format, trackIndex, selection, group);
|
||||
|
||||
|
||||
boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-");
|
||||
boolean isSelected = isTrackSelected(selection, group, trackIndex);
|
||||
|
||||
|
||||
textTrack.setIndex(textTracks.size());
|
||||
|
||||
|
||||
if (textTrack.getTitle() == null || textTrack.getTitle().isEmpty()) {
|
||||
if (isExternal) {
|
||||
textTrack.setTitle("External " + (trackIndex + 1));
|
||||
|
|
@ -1765,7 +1765,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
textTrack.setTitle("Track " + (textTracks.size() + 1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
textTracks.add(textTrack);
|
||||
}
|
||||
}
|
||||
|
|
@ -1785,11 +1785,11 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
}
|
||||
|
||||
TrackGroupArray groups = info.getTrackGroups(index);
|
||||
|
||||
|
||||
for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
|
||||
TrackGroup group = groups.get(groupIndex);
|
||||
Format format = group.getFormat(0);
|
||||
|
||||
|
||||
// Create track without trying to determine selection status
|
||||
Track track = new Track();
|
||||
track.setIndex(groupIndex);
|
||||
|
|
@ -1799,10 +1799,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
if (format.sampleMimeType != null)
|
||||
track.setMimeType(format.sampleMimeType);
|
||||
track.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
||||
|
||||
|
||||
tracks.add(track);
|
||||
}
|
||||
|
||||
|
||||
DebugLog.d(TAG, "getBasicAudioTrackInfo: returning " + tracks.size() + " audio tracks (no selection status)");
|
||||
return tracks;
|
||||
}
|
||||
|
|
@ -1812,29 +1812,29 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
if (trackSelector == null) {
|
||||
return textTracks;
|
||||
}
|
||||
|
||||
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
int index = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (info == null || index == C.INDEX_UNSET) {
|
||||
return textTracks;
|
||||
}
|
||||
|
||||
|
||||
TrackGroupArray groups = info.getTrackGroups(index);
|
||||
|
||||
for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
|
||||
TrackGroup group = groups.get(groupIndex);
|
||||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
||||
Format format = group.getFormat(trackIndex);
|
||||
|
||||
|
||||
Track textTrack = new Track();
|
||||
textTrack.setIndex(textTracks.size());
|
||||
if (format.sampleMimeType != null)
|
||||
textTrack.setMimeType(format.sampleMimeType);
|
||||
if (format.language != null)
|
||||
textTrack.setLanguage(format.language);
|
||||
|
||||
|
||||
boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-");
|
||||
|
||||
|
||||
if (format.label != null && !format.label.isEmpty()) {
|
||||
textTrack.setTitle(format.label);
|
||||
} else if (isExternal) {
|
||||
|
|
@ -1842,7 +1842,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
} else {
|
||||
textTrack.setTitle("Track " + (textTracks.size() + 1));
|
||||
}
|
||||
|
||||
|
||||
textTrack.setSelected(false); // Don't report selection status - let PlayerView handle it
|
||||
textTracks.add(textTrack);
|
||||
}
|
||||
|
|
@ -1912,12 +1912,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
@Override
|
||||
public void onTracksChanged(@NonNull Tracks tracks) {
|
||||
DebugLog.d(TAG, "onTracksChanged called - updating track information, controls=" + controls);
|
||||
|
||||
|
||||
if (controls) {
|
||||
ArrayList<Track> textTracks = getBasicTextTrackInfo();
|
||||
ArrayList<Track> audioTracks = getBasicAudioTrackInfo();
|
||||
ArrayList<Track> audioTracks = getBasicAudioTrackInfo();
|
||||
ArrayList<VideoTrack> videoTracks = getVideoTrackInfo();
|
||||
|
||||
|
||||
eventEmitter.onTextTracks.invoke(textTracks);
|
||||
eventEmitter.onAudioTracks.invoke(audioTracks);
|
||||
eventEmitter.onVideoTracks.invoke(videoTracks);
|
||||
|
|
@ -1925,7 +1925,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
ArrayList<Track> textTracks = getTextTrackInfo();
|
||||
ArrayList<Track> audioTracks = getAudioTrackInfo();
|
||||
ArrayList<VideoTrack> videoTracks = getVideoTrackInfo();
|
||||
|
||||
|
||||
eventEmitter.onTextTracks.invoke(textTracks);
|
||||
eventEmitter.onAudioTracks.invoke(audioTracks);
|
||||
eventEmitter.onVideoTracks.invoke(videoTracks);
|
||||
|
|
@ -1936,24 +1936,24 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateSubtitleButtonVisibility();
|
||||
}
|
||||
|
||||
|
||||
private boolean hasBuiltInTextTracks() {
|
||||
if (player == null || trackSelector == null)
|
||||
return false;
|
||||
|
||||
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (info == null)
|
||||
return false;
|
||||
|
||||
|
||||
int textRendererIndex = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (textRendererIndex == C.INDEX_UNSET)
|
||||
return false;
|
||||
|
||||
|
||||
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
|
||||
|
||||
|
||||
// Check if any groups have tracks that are NOT external subtitles
|
||||
for (int i = 0; i < groups.length; i++) {
|
||||
TrackGroup group = groups.get(i);
|
||||
|
|
@ -1965,18 +1965,18 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateSubtitleButtonVisibility() {
|
||||
if (exoPlayerView == null)
|
||||
return;
|
||||
|
||||
boolean hasTextTracks = (source.getSideLoadedTextTracks() != null &&
|
||||
|
||||
boolean hasTextTracks = (source.getSideLoadedTextTracks() != null &&
|
||||
!source.getSideLoadedTextTracks().getTracks().isEmpty()) ||
|
||||
hasBuiltInTextTracks();
|
||||
|
||||
|
||||
exoPlayerView.setShowSubtitleButton(hasTextTracks);
|
||||
}
|
||||
|
||||
|
|
@ -2177,7 +2177,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
public void disableTrack(int rendererIndex) {
|
||||
if (trackSelector == null)
|
||||
return;
|
||||
|
||||
|
||||
DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters()
|
||||
.buildUpon()
|
||||
.setRendererDisabled(rendererIndex, true)
|
||||
|
|
@ -2188,25 +2188,25 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
private void selectTextTrackInternal(String type, String value) {
|
||||
if (player == null || trackSelector == null)
|
||||
return;
|
||||
|
||||
|
||||
DebugLog.d(TAG, "selectTextTrackInternal: type=" + type + ", value=" + value);
|
||||
|
||||
|
||||
DefaultTrackSelector.Parameters.Builder parametersBuilder = trackSelector.getParameters().buildUpon();
|
||||
|
||||
|
||||
if ("disabled".equals(type) || value == null) {
|
||||
parametersBuilder.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true);
|
||||
} else {
|
||||
parametersBuilder.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false);
|
||||
|
||||
|
||||
parametersBuilder.clearOverridesOfType(C.TRACK_TYPE_TEXT);
|
||||
|
||||
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (info != null) {
|
||||
int textRendererIndex = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (textRendererIndex != C.INDEX_UNSET) {
|
||||
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
|
||||
boolean trackFound = false;
|
||||
|
||||
|
||||
// react-native-video uses a flattened `textTracks` list on the JS side.
|
||||
// For HLS/DASH, each TrackGroup often contains a single track at index 0,
|
||||
// so comparing against `trackIndex` alone makes only the first subtitle selectable.
|
||||
|
|
@ -2216,7 +2216,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
TrackGroup group = groups.get(groupIndex);
|
||||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
||||
Format format = group.getFormat(trackIndex);
|
||||
|
||||
|
||||
boolean isMatch = false;
|
||||
if ("language".equals(type) && format.language != null && format.language.equals(value)) {
|
||||
isMatch = true;
|
||||
|
|
@ -2228,11 +2228,11 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
isMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
flattenedIndex++;
|
||||
|
||||
if (isMatch) {
|
||||
TrackSelectionOverride override = new TrackSelectionOverride(group,
|
||||
TrackSelectionOverride override = new TrackSelectionOverride(group,
|
||||
java.util.Arrays.asList(trackIndex));
|
||||
parametersBuilder.addOverride(override);
|
||||
trackFound = true;
|
||||
|
|
@ -2242,18 +2242,18 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
if (trackFound)
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (!trackFound) {
|
||||
DebugLog.w(TAG, "Text track not found for type=" + type + ", value=" + value +
|
||||
DebugLog.w(TAG, "Text track not found for type=" + type + ", value=" + value +
|
||||
". Keeping current selection.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
trackSelector.setParameters(parametersBuilder.build());
|
||||
|
||||
|
||||
// Give PlayerView time to update its controls
|
||||
mainHandler.postDelayed(() -> {
|
||||
if (exoPlayerView != null) {
|
||||
|
|
@ -2268,16 +2268,16 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
public void setSelectedTrack(int trackType, String type, String value) {
|
||||
if (player == null || trackSelector == null)
|
||||
return;
|
||||
|
||||
|
||||
if (controls) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
int rendererIndex = getTrackRendererIndex(trackType);
|
||||
if (rendererIndex == C.INDEX_UNSET) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (info == null) {
|
||||
return;
|
||||
|
|
@ -2435,7 +2435,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
.setExceedVideoConstraintsIfNecessary(true)
|
||||
.setRendererDisabled(rendererIndex, false);
|
||||
|
||||
// Clear existing overrides for this track type to avoid conflicts
|
||||
// Clear existing overrides for this track type to avoid conflicts
|
||||
// But be careful with audio tracks - don't clear unless explicitly selecting a
|
||||
// different track
|
||||
if (trackType != C.TRACK_TYPE_AUDIO || !type.equals("default")) {
|
||||
|
|
@ -2452,7 +2452,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||
selectionParameters.setForceHighestSupportedBitrate(false);
|
||||
selectionParameters.setForceLowestBitrate(false);
|
||||
DebugLog.d(TAG, "Audio track selection: group=" + groupIndex + ", tracks=" + tracks +
|
||||
DebugLog.d(TAG, "Audio track selection: group=" + groupIndex + ", tracks=" + tracks +
|
||||
", override=" + selectionOverride);
|
||||
}
|
||||
|
||||
|
|
@ -2512,7 +2512,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
public void setSelectedAudioTrack(String type, String value) {
|
||||
audioTrackType = type;
|
||||
audioTrackValue = value;
|
||||
|
||||
|
||||
if (!controls && player != null && trackSelector != null) {
|
||||
setSelectedTrack(C.TRACK_TYPE_AUDIO, audioTrackType, audioTrackValue);
|
||||
}
|
||||
|
|
@ -2521,7 +2521,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
public void setSelectedTextTrack(String type, String value) {
|
||||
textTrackType = type;
|
||||
textTrackValue = value;
|
||||
|
||||
|
||||
selectTextTrackInternal(type, value);
|
||||
}
|
||||
|
||||
|
|
@ -2847,7 +2847,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
"code", String.valueOf(error.getErrorCode()),
|
||||
"type", String.valueOf(error.getErrorType()));
|
||||
eventEmitter.onReceiveAdEvent.invoke("ERROR", errMap);
|
||||
|
||||
|
||||
handleDaiBackupStream();
|
||||
}
|
||||
|
||||
|
|
@ -2855,7 +2855,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
controlsConfig = controlsStyles;
|
||||
refreshControlsStyles();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the source is a DAI (Dynamic Ad Insertion) request.
|
||||
*
|
||||
|
|
@ -2902,7 +2902,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
daiAdsLoader, mediaSourceFactory);
|
||||
|
||||
mediaSourceFactory.setServerSideAdInsertionMediaSourceFactory(adsMediaSourceFactory);
|
||||
|
||||
|
||||
return mediaSourceFactory;
|
||||
}
|
||||
|
||||
|
|
@ -2944,15 +2944,15 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
eventEmitter.onVideoError.invoke("DaiAdsLoader is null", null, "DAI_ADS_LOADER_NULL_ERROR");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
daiAdsLoader.setPlayer(player);
|
||||
|
||||
|
||||
AdsProps adsProps = runningSource.getAdsProps();
|
||||
int streamFormat = "dash".equalsIgnoreCase(adsProps.getFormat()) ? CONTENT_TYPE_DASH : CONTENT_TYPE_HLS;
|
||||
|
||||
|
||||
try {
|
||||
Uri.Builder uriBuilder;
|
||||
|
||||
|
||||
if (adsProps.isDAILive()) {
|
||||
uriBuilder = new ImaServerSideAdInsertionUriBuilder()
|
||||
.setAssetKey(adsProps.getAssetKey())
|
||||
|
|
@ -2970,17 +2970,17 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
throw new IllegalArgumentException(
|
||||
"Either assetKey (for live) or contentSourceId+videoId (for VOD) must be provided");
|
||||
}
|
||||
|
||||
|
||||
Map<String, String> adTagParameters = adsProps.getAdTagParameters();
|
||||
if (adTagParameters != null && !adTagParameters.isEmpty()) {
|
||||
for (Map.Entry<String, String> entry : adTagParameters.entrySet()) {
|
||||
uriBuilder.appendQueryParameter(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Uri ssaiUri = uriBuilder.build();
|
||||
MediaItem ssaiMediaItem = MediaItem.fromUri(ssaiUri);
|
||||
|
||||
|
||||
player.setMediaItem(ssaiMediaItem);
|
||||
} catch (Exception e) {
|
||||
eventEmitter.onVideoError.invoke("DAI stream request failed: " + e.getMessage(), e, "DAI_REQUEST_ERROR");
|
||||
|
|
@ -3001,29 +3001,29 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
if (source == null || source.getAdsProps() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
String fallbackStreamUri = source.getAdsProps().getFallbackUri();
|
||||
if (fallbackStreamUri == null || fallbackStreamUri.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
DebugLog.d(TAG, "DAI stream error occurred, falling back to backup stream URI: " + fallbackStreamUri);
|
||||
|
||||
|
||||
WritableMap backupSourceMap = Arguments.createMap();
|
||||
backupSourceMap.putString("uri", fallbackStreamUri);
|
||||
backupSourceMap.putBoolean("isNetwork", true);
|
||||
|
||||
|
||||
Source backupSource = Source.parse(backupSourceMap, themedReactContext);
|
||||
if (backupSource == null || backupSource.getUri() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (daiAdsLoader != null) {
|
||||
daiAdsLoader.setPlayer(null);
|
||||
}
|
||||
|
||||
|
||||
setSrc(backupSource);
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
132
package-lock.json
generated
132
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "nuvio",
|
||||
"version": "0.6.0-beta.6",
|
||||
"version": "0.6.0-beta.16",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nuvio",
|
||||
"version": "0.6.0-beta.6",
|
||||
"version": "0.6.0-beta.16",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@adrianso/react-native-device-brightness": "^1.2.7",
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
"@gorhom/bottom-sheet": "^5.2.6",
|
||||
"@kesha-antonov/react-native-background-downloader": "^4.4.5",
|
||||
"@legendapp/list": "^2.0.13",
|
||||
"@lottiefiles/dotlottie-react": "^0.17.7",
|
||||
"@lottiefiles/dotlottie-react": "^0.13.5",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-native-community/slider": "^5.1.1",
|
||||
|
|
@ -1540,6 +1540,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
|
@ -1798,6 +1799,15 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli/node_modules/undici": {
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/code-signing-certificates": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz",
|
||||
|
|
@ -2088,6 +2098,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-6.1.2.tgz",
|
||||
"integrity": "sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"anser": "^1.4.9",
|
||||
"pretty-format": "^29.7.0",
|
||||
|
|
@ -2576,6 +2587,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz",
|
||||
"integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jimp/core": "^0.22.12"
|
||||
}
|
||||
|
|
@ -2757,21 +2769,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@lottiefiles/dotlottie-react": {
|
||||
"version": "0.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.17.10.tgz",
|
||||
"integrity": "sha512-ikrN05/q0/KjqIU+n48uNwmE7DeZIC9y3Nd19httcKqe273zoOeNYycEaQzLSdcpEGnWLmHaZpgtoo07aQZAXg==",
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.13.5.tgz",
|
||||
"integrity": "sha512-4U5okwjRqDPkjB572hfZtLXJ/LGfCo6vDwUB2KIPEUoSgqbIlw+UrbnaqVp3GS+dRvhMD27F2JObpHpYRlpF0Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@lottiefiles/dotlottie-web": "0.58.1"
|
||||
"@lottiefiles/dotlottie-web": "0.44.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@lottiefiles/dotlottie-web": {
|
||||
"version": "0.58.1",
|
||||
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.58.1.tgz",
|
||||
"integrity": "sha512-YC4pmScrV0R3rd11gU5xHrjeNczlCic69zlnMH/buDIzYxIbpR88oPUhGtKgu5ln7EJchoLpeRJbA3uLCzSeTA==",
|
||||
"version": "0.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.44.0.tgz",
|
||||
"integrity": "sha512-IUWKVciDJI/BMWDWnh7j0Ngd0N8q9ySRAwm84aDqIE07qpmdZ7x1rkIpBaU1yHSNqNYHeh1Rxsl+LC3CY4f0KA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
|
|
@ -3116,7 +3129,7 @@
|
|||
"version": "0.72.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz",
|
||||
"integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4",
|
||||
|
|
@ -3237,6 +3250,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.25.tgz",
|
||||
"integrity": "sha512-zQeWK9txDePWbYfqTs0C6jeRdJTm/7VhQtW/1IbJNDi9/rFIRzZule8bdQPAnf8QWUsNujRmi1J9OG/hhfbalg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^7.13.6",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
|
|
@ -3878,6 +3892,7 @@
|
|||
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@svgr/babel-preset": "8.1.0",
|
||||
|
|
@ -4098,6 +4113,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
|
|
@ -4107,8 +4123,9 @@
|
|||
"version": "0.72.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.72.8.tgz",
|
||||
"integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@react-native/virtualized-lists": "^0.72.4",
|
||||
"@types/react": "*"
|
||||
|
|
@ -4644,6 +4661,7 @@
|
|||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
|
|
@ -5047,6 +5065,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
|
|
@ -6276,6 +6295,7 @@
|
|||
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.29.tgz",
|
||||
"integrity": "sha512-9C90gyOzV83y2S3XzCbRDCuKYNaiyCzuP9ketv46acHCEZn+QTamPK/DobdghoSiofCmlfoaiD6/SzfxDiHMnw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.0",
|
||||
"@expo/cli": "54.0.19",
|
||||
|
|
@ -6479,6 +6499,7 @@
|
|||
"resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.10.tgz",
|
||||
"integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ua-parser-js": "^0.7.33"
|
||||
},
|
||||
|
|
@ -6506,6 +6527,7 @@
|
|||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
|
||||
"integrity": "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react-native": "*"
|
||||
|
|
@ -6516,6 +6538,7 @@
|
|||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.10.tgz",
|
||||
"integrity": "sha512-UqyNaaLKRpj4pKAP4HZSLnuDQqueaO5tB1c/NWu5vh1/LF9ulItyyg2kF/IpeOp0DeOLk0GY0HrIXaKUMrwB+Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fontfaceobserver": "^2.1.0"
|
||||
},
|
||||
|
|
@ -6611,6 +6634,7 @@
|
|||
"resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-17.0.8.tgz",
|
||||
"integrity": "sha512-UrdwklZBDJ+t+ZszMMiE0SXZ2eJxcquCuQcl6EvGHM9K+e6YqKVRQ+w8qE+iIB3H75v2RJy6MHAaLK+Mqeo04g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"rtl-detect": "^1.0.2"
|
||||
},
|
||||
|
|
@ -7670,6 +7694,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
|
|
@ -8286,6 +8311,20 @@
|
|||
"xml-name-validator": ">= 2.0.1 < 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/tough-cookie": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"psl": "^1.1.28",
|
||||
"punycode": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
|
|
@ -10562,6 +10601,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -10602,6 +10642,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
|
|
@ -10659,6 +10700,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.4.tgz",
|
||||
"integrity": "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/create-cache-key-function": "^29.7.0",
|
||||
"@react-native/assets-registry": "0.81.4",
|
||||
|
|
@ -10747,6 +10789,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-bottom-tabs/-/react-native-bottom-tabs-1.1.0.tgz",
|
||||
"integrity": "sha512-Uu1gvM3i1Hb4DjVvR/38J1QVQEs0RkPc7K6yon99HgvRWWOyLs7kjPDhUswtb8ije4pKW712skIXWJ0lgKzbyQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"react-freeze": "^1.0.0",
|
||||
"sf-symbols-typescript": "^2.0.0",
|
||||
|
|
@ -10777,6 +10820,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.29.1.tgz",
|
||||
"integrity": "sha512-du3qmv0e3Sm7qsd9SfmHps+AggLiylcBBQ8ztz7WUtd8ZjKs5V3kekAbi9R2W9bRLSg47Ntp4GGMYZOhikQdZA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@egjs/hammerjs": "^2.0.17",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
|
|
@ -10875,6 +10919,7 @@
|
|||
"integrity": "sha512-hcvjTu9YJE9fMmnAUvhG8CxvYLpOuMQ/2eyi/S6GyrecezF6Rmk/uRQEL6v09BRFWA/xRVZNQVulQPS+2HS3mQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
|
|
@ -10940,6 +10985,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.2.0.tgz",
|
||||
"integrity": "sha512-frhu5b8/m/VvaMWz48V8RxcsXnE3hrlErQ5chr21MzAeDCpY4X14sQjvm+jvu3aOI+7Cz2atdRpyhhIuqxVaXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"react-native-is-edge-to-edge": "1.2.1",
|
||||
"semver": "7.7.3"
|
||||
|
|
@ -10979,6 +11025,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
||||
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
|
|
@ -10989,6 +11036,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.18.0.tgz",
|
||||
"integrity": "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"react-freeze": "^1.0.0",
|
||||
"warn-once": "^0.1.0"
|
||||
|
|
@ -11003,6 +11051,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.1.tgz",
|
||||
"integrity": "sha512-ZUD1xwc3Hwo4cOmOLumjJVoc7lEf9oQFlHnLmgccLC19fNm6LVEdtB+Cnip6gEi0PG3wfvVzskViEtrySQP8Fw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"css-select": "^5.1.0",
|
||||
"css-tree": "^1.1.3",
|
||||
|
|
@ -11231,6 +11280,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
||||
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"@react-native/normalize-colors": "^0.74.1",
|
||||
|
|
@ -11487,6 +11537,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -11690,6 +11741,20 @@
|
|||
"node": ">= 0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/request/node_modules/tough-cookie": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"psl": "^1.1.28",
|
||||
"punycode": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
@ -12939,6 +13004,24 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.0.19",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz",
|
||||
"integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.0.19"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.0.19",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
|
||||
"integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
|
|
@ -12994,17 +13077,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
|
||||
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"psl": "^1.1.28",
|
||||
"punycode": "^2.1.1"
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
|
|
@ -13081,8 +13163,9 @@
|
|||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -13123,15 +13206,6 @@
|
|||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"name": "nuvio",
|
||||
"version": "0.6.0-beta.6",
|
||||
"version": "0.6.0-beta.16",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"build": "export NODE_ENV=production && cd android && ./gradlew assembleRelease",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -19,7 +20,7 @@
|
|||
"@gorhom/bottom-sheet": "^5.2.6",
|
||||
"@kesha-antonov/react-native-background-downloader": "^4.4.5",
|
||||
"@legendapp/list": "^2.0.13",
|
||||
"@lottiefiles/dotlottie-react": "^0.17.7",
|
||||
"@lottiefiles/dotlottie-react": "^0.13.5",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-native-community/slider": "^5.1.1",
|
||||
|
|
@ -110,5 +111,8 @@
|
|||
"typescript": "^5.9.3",
|
||||
"xcode": "^3.0.1"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "~18.3.12"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
|
|
|||
38
patches/react-native-bottom-tabs+1.1.0.patch
Normal file
38
patches/react-native-bottom-tabs+1.1.0.patch
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
diff --git a/node_modules/react-native-bottom-tabs/app.plugin.js b/node_modules/react-native-bottom-tabs/app.plugin.js
|
||||
index 87edf84..711d0c0 100644
|
||||
--- a/node_modules/react-native-bottom-tabs/app.plugin.js
|
||||
+++ b/node_modules/react-native-bottom-tabs/app.plugin.js
|
||||
@@ -1 +1,31 @@
|
||||
-module.exports = require('./lib/module/expo');
|
||||
+"use strict";
|
||||
+
|
||||
+const { createRunOncePlugin, withAndroidStyles } = require("@expo/config-plugins");
|
||||
+
|
||||
+const MATERIAL3_THEME_DYANMIC = "Theme.Material3.DynamicColors.DayNight.NoActionBar";
|
||||
+const MATERIAL3_THEME = "Theme.Material3.DayNight.NoActionBar";
|
||||
+const MATERIAL2_THEME = "Theme.MaterialComponents.DayNight.NoActionBar";
|
||||
+const MATERIAL3_EXPRESSIVE_THEME = "Theme.Material3Expressive.DayNight.NoActionBar";
|
||||
+
|
||||
+const withMaterial3Theme = (config, options) => {
|
||||
+ const theme = options?.theme;
|
||||
+ return withAndroidStyles(config, stylesConfig => {
|
||||
+ stylesConfig.modResults.resources.style = stylesConfig.modResults.resources.style?.map(style => {
|
||||
+ if (style.$.name === "AppTheme") {
|
||||
+ if (theme === "material3-dynamic") {
|
||||
+ style.$.parent = MATERIAL3_THEME_DYANMIC;
|
||||
+ } else if (theme === "material2") {
|
||||
+ style.$.parent = MATERIAL2_THEME;
|
||||
+ } else if (theme === "material3-expressive") {
|
||||
+ style.$.parent = MATERIAL3_EXPRESSIVE_THEME;
|
||||
+ } else {
|
||||
+ style.$.parent = MATERIAL3_THEME;
|
||||
+ }
|
||||
+ }
|
||||
+ return style;
|
||||
+ });
|
||||
+ return stylesConfig;
|
||||
+ });
|
||||
+};
|
||||
+
|
||||
+module.exports = createRunOncePlugin(withMaterial3Theme, "react-native-bottom-tabs");
|
||||
\ No newline at end of file
|
||||
|
|
@ -35,6 +35,7 @@ import { AudioTrackModal } from './modals/AudioTrackModal';
|
|||
import { SubtitleModals } from './modals/SubtitleModals';
|
||||
import { SubtitleSyncModal } from './modals/SubtitleSyncModal';
|
||||
import SpeedModal from './modals/SpeedModal';
|
||||
import { SubmitIntroModal } from './modals/SubmitIntroModal';
|
||||
import { SourcesModal } from './modals/SourcesModal';
|
||||
import { EpisodesModal } from './modals/EpisodesModal';
|
||||
import { EpisodeStreamsModal } from './modals/EpisodeStreamsModal';
|
||||
|
|
@ -917,6 +918,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
setShowAudioModal={modals.setShowAudioModal}
|
||||
setShowSubtitleModal={modals.setShowSubtitleModal}
|
||||
setShowSpeedModal={modals.setShowSpeedModal}
|
||||
setShowSubmitIntroModal={modals.setShowSubmitIntroModal}
|
||||
isSubtitleModalOpen={modals.showSubtitleModal}
|
||||
setShowSourcesModal={modals.setShowSourcesModal}
|
||||
setShowEpisodesModal={type === 'series' ? modals.setShowEpisodesModal : undefined}
|
||||
|
|
@ -932,6 +934,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
onSwitchToMPV={handleManualSwitchToMPV}
|
||||
useExoPlayer={useExoPlayer}
|
||||
isBuffering={playerState.isBuffering}
|
||||
imdbId={imdbId}
|
||||
/>
|
||||
|
||||
<SpeedActivatedOverlay
|
||||
|
|
@ -1106,6 +1109,15 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
setHoldToSpeedValue={speedControl.setHoldToSpeedValue}
|
||||
/>
|
||||
|
||||
<SubmitIntroModal
|
||||
visible={modals.showSubmitIntroModal}
|
||||
onClose={() => modals.setShowSubmitIntroModal(false)}
|
||||
currentTime={playerState.currentTime}
|
||||
imdbId={imdbId}
|
||||
season={season}
|
||||
episode={episode}
|
||||
/>
|
||||
|
||||
<EpisodesModal
|
||||
showEpisodesModal={modals.showEpisodesModal}
|
||||
setShowEpisodesModal={modals.setShowEpisodesModal}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import UpNextButton from './common/UpNextButton';
|
|||
import { PlayerControls } from './controls/PlayerControls';
|
||||
import AudioTrackModal from './modals/AudioTrackModal';
|
||||
import SpeedModal from './modals/SpeedModal';
|
||||
import { SubmitIntroModal } from './modals/SubmitIntroModal';
|
||||
import SubtitleModals from './modals/SubtitleModals';
|
||||
import { SubtitleSyncModal } from './modals/SubtitleSyncModal';
|
||||
import SourcesModal from './modals/SourcesModal';
|
||||
|
|
@ -868,6 +869,7 @@ const KSPlayerCore: React.FC = () => {
|
|||
setShowAudioModal={modals.setShowAudioModal}
|
||||
setShowSubtitleModal={modals.setShowSubtitleModal}
|
||||
setShowSpeedModal={modals.setShowSpeedModal}
|
||||
setShowSubmitIntroModal={modals.setShowSubmitIntroModal}
|
||||
isSubtitleModalOpen={modals.showSubtitleModal}
|
||||
setShowSourcesModal={modals.setShowSourcesModal}
|
||||
setShowEpisodesModal={type === 'series' ? modals.setShowEpisodesModal : undefined}
|
||||
|
|
@ -881,6 +883,7 @@ const KSPlayerCore: React.FC = () => {
|
|||
allowsAirPlay={allowsAirPlay}
|
||||
onAirPlayPress={() => ksPlayerRef.current?.showAirPlayPicker()}
|
||||
isBuffering={isBuffering}
|
||||
imdbId={imdbId}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -998,6 +1001,15 @@ const KSPlayerCore: React.FC = () => {
|
|||
setHoldToSpeedValue={speedControl.setHoldToSpeedValue}
|
||||
/>
|
||||
|
||||
<SubmitIntroModal
|
||||
visible={modals.showSubmitIntroModal}
|
||||
onClose={() => modals.setShowSubmitIntroModal(false)}
|
||||
currentTime={currentTime}
|
||||
imdbId={imdbId}
|
||||
season={season}
|
||||
episode={episode}
|
||||
/>
|
||||
|
||||
<SubtitleModals
|
||||
showSubtitleModal={modals.showSubtitleModal}
|
||||
setShowSubtitleModal={modals.setShowSubtitleModal}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ import { useTranslation } from 'react-i18next';
|
|||
import { styles } from '../utils/playerStyles'; // Updated styles
|
||||
import { getTrackDisplayName } from '../utils/playerUtils';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { useSettings } from '../../../hooks/useSettings';
|
||||
|
||||
import { introService } from '../../../services/introService';
|
||||
import { toastService } from '../../../services/toastService';
|
||||
|
||||
interface PlayerControlsProps {
|
||||
showControls: boolean;
|
||||
|
|
@ -37,6 +41,7 @@ interface PlayerControlsProps {
|
|||
setShowAudioModal: (show: boolean) => void;
|
||||
setShowSubtitleModal: (show: boolean) => void;
|
||||
setShowSpeedModal: (show: boolean) => void;
|
||||
setShowSubmitIntroModal: (show: boolean) => void;
|
||||
isSubtitleModalOpen?: boolean;
|
||||
setShowSourcesModal?: (show: boolean) => void;
|
||||
setShowEpisodesModal?: (show: boolean) => void;
|
||||
|
|
@ -55,6 +60,7 @@ interface PlayerControlsProps {
|
|||
onSwitchToMPV?: () => void;
|
||||
useExoPlayer?: boolean;
|
||||
isBuffering?: boolean;
|
||||
imdbId?: string;
|
||||
}
|
||||
|
||||
export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||
|
|
@ -85,6 +91,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
setShowAudioModal,
|
||||
setShowSubtitleModal,
|
||||
setShowSpeedModal,
|
||||
setShowSubmitIntroModal,
|
||||
isSubtitleModalOpen,
|
||||
setShowSourcesModal,
|
||||
setShowEpisodesModal,
|
||||
|
|
@ -100,10 +107,17 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
onSwitchToMPV,
|
||||
useExoPlayer,
|
||||
isBuffering = false,
|
||||
imdbId,
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// --- Intro Submission Logic ---
|
||||
const handleIntroPress = () => {
|
||||
setShowSubmitIntroModal(true);
|
||||
};
|
||||
|
||||
|
||||
/* Responsive Spacing */
|
||||
const screenWidth = Dimensions.get('window').width;
|
||||
|
|
@ -619,6 +633,20 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Submit Intro Button */}
|
||||
{season !== undefined && episode !== undefined && settings.introSubmitEnabled && settings.introDbApiKey && (
|
||||
<TouchableOpacity
|
||||
style={styles.iconButton}
|
||||
onPress={handleIntroPress}
|
||||
>
|
||||
<Ionicons
|
||||
name="flag-outline"
|
||||
size={24}
|
||||
color="white"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Right Side: Episodes Button */}
|
||||
{setShowEpisodesModal && (
|
||||
<TouchableOpacity
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const usePlayerModals = () => {
|
|||
const [showErrorModal, setShowErrorModal] = useState(false);
|
||||
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState(false);
|
||||
const [showCastDetails, setShowCastDetails] = useState(false);
|
||||
const [showSubmitIntroModal, setShowSubmitIntroModal] = useState(false);
|
||||
|
||||
// Some modals have associated data
|
||||
const [selectedEpisodeForStreams, setSelectedEpisodeForStreams] = useState<Episode | null>(null);
|
||||
|
|
@ -31,6 +32,7 @@ export const usePlayerModals = () => {
|
|||
showErrorModal, setShowErrorModal,
|
||||
showSubtitleLanguageModal, setShowSubtitleLanguageModal,
|
||||
showCastDetails, setShowCastDetails,
|
||||
showSubmitIntroModal, setShowSubmitIntroModal,
|
||||
selectedEpisodeForStreams, setSelectedEpisodeForStreams,
|
||||
errorDetails, setErrorDetails,
|
||||
selectedCastMember, setSelectedCastMember
|
||||
|
|
|
|||
347
src/components/player/modals/SubmitIntroModal.tsx
Normal file
347
src/components/player/modals/SubmitIntroModal.tsx
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, TouchableOpacity, useWindowDimensions, StyleSheet, TextInput, ActivityIndicator } from 'react-native';
|
||||
import { Ionicons, MaterialIcons } from '@expo/vector-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
SlideInDown,
|
||||
SlideOutDown,
|
||||
} from 'react-native-reanimated';
|
||||
import { useSettings } from '../../../hooks/useSettings';
|
||||
import { introService } from '../../../services/introService';
|
||||
import { toastService } from '../../../services/toastService';
|
||||
|
||||
interface SubmitIntroModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
currentTime: number;
|
||||
imdbId?: string;
|
||||
season?: number;
|
||||
episode?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses time string (MM:SS or SS) to seconds
|
||||
*/
|
||||
const parseTimeToSeconds = (input: string): number | null => {
|
||||
if (!input) return null;
|
||||
|
||||
// Format: MM:SS
|
||||
if (input.includes(':')) {
|
||||
const parts = input.split(':');
|
||||
if (parts.length !== 2) return null;
|
||||
const mins = parseInt(parts[0], 10);
|
||||
const secs = parseInt(parts[1], 10);
|
||||
if (isNaN(mins) || isParseSecs(secs)) return null;
|
||||
return mins * 60 + secs;
|
||||
}
|
||||
|
||||
// Format: Seconds only
|
||||
const secs = parseInt(input, 10);
|
||||
return isNaN(secs) ? null : secs;
|
||||
};
|
||||
|
||||
const isParseSecs = (secs: number) => isNaN(secs) || secs < 0 || secs >= 60;
|
||||
|
||||
/**
|
||||
* Formats seconds to MM:SS
|
||||
*/
|
||||
const formatSecondsToMMSS = (seconds: number): string => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
export const SubmitIntroModal: React.FC<SubmitIntroModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
currentTime,
|
||||
imdbId,
|
||||
season,
|
||||
episode,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { width } = useWindowDimensions();
|
||||
const { settings } = useSettings();
|
||||
|
||||
const [startTimeStr, setStartTimeStr] = useState('00:00');
|
||||
const [endTimeStr, setEndTimeStr] = useState(formatSecondsToMMSS(currentTime));
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setEndTimeStr(formatSecondsToMMSS(currentTime));
|
||||
}
|
||||
}, [visible, currentTime]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
const handleCaptureStart = () => setStartTimeStr(formatSecondsToMMSS(currentTime));
|
||||
const handleCaptureEnd = () => setEndTimeStr(formatSecondsToMMSS(currentTime));
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const startSec = parseTimeToSeconds(startTimeStr);
|
||||
const endSec = parseTimeToSeconds(endTimeStr);
|
||||
|
||||
if (startSec === null || endSec === null) {
|
||||
toastService.error('Invalid format', 'Please use MM:SS format');
|
||||
return;
|
||||
}
|
||||
|
||||
if (endSec <= startSec) {
|
||||
toastService.warning('Invalid duration', 'End time must be after start time');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!imdbId || season === undefined || episode === undefined) {
|
||||
toastService.error('Missing metadata', 'Could not identify this episode');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const success = await introService.submitIntro(
|
||||
settings.introDbApiKey,
|
||||
imdbId,
|
||||
season,
|
||||
episode,
|
||||
startSec,
|
||||
endSec
|
||||
);
|
||||
|
||||
if (success) {
|
||||
toastService.success(t('player_ui.intro_submitted', { defaultValue: 'Intro submitted successfully' }));
|
||||
onClose();
|
||||
} else {
|
||||
toastService.error(t('player_ui.intro_submit_failed', { defaultValue: 'Failed to submit intro' }));
|
||||
}
|
||||
} catch (error) {
|
||||
toastService.error('Error', 'An unexpected error occurred');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startVal = parseTimeToSeconds(startTimeStr);
|
||||
const endVal = parseTimeToSeconds(endTimeStr);
|
||||
const durationSec = (startVal !== null && endVal !== null) ? endVal - startVal : 0;
|
||||
|
||||
return (
|
||||
<View style={[StyleSheet.absoluteFill, { zIndex: 10000 }]}>
|
||||
<TouchableOpacity
|
||||
style={StyleSheet.absoluteFill}
|
||||
activeOpacity={1}
|
||||
onPress={onClose}
|
||||
>
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View pointerEvents="box-none" style={localStyles.centeredView}>
|
||||
<Animated.View
|
||||
entering={SlideInDown.duration(300)}
|
||||
exiting={SlideOutDown.duration(250)}
|
||||
style={[localStyles.modalContainer, { width: Math.min(width * 0.85, 380) }]}
|
||||
>
|
||||
<View style={localStyles.header}>
|
||||
<Text style={localStyles.title}>Submit Intro Timestamp</Text>
|
||||
<TouchableOpacity onPress={onClose} style={localStyles.closeButton}>
|
||||
<Ionicons name="close" size={24} color="rgba(255,255,255,0.5)" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={localStyles.content}>
|
||||
{/* Start Time Input */}
|
||||
<View style={localStyles.inputRow}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={localStyles.label}>Start Time (MM:SS)</Text>
|
||||
<TextInput
|
||||
style={localStyles.input}
|
||||
value={startTimeStr}
|
||||
onChangeText={setStartTimeStr}
|
||||
placeholder="00:00"
|
||||
placeholderTextColor="rgba(255,255,255,0.3)"
|
||||
keyboardType="numbers-and-punctuation"
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity onPress={handleCaptureStart} style={localStyles.captureBtn}>
|
||||
<MaterialIcons name="my-location" size={20} color="white" />
|
||||
<Text style={localStyles.captureText}>Capture</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* End Time Input */}
|
||||
<View style={localStyles.inputRow}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={localStyles.label}>End Time (MM:SS)</Text>
|
||||
<TextInput
|
||||
style={localStyles.input}
|
||||
value={endTimeStr}
|
||||
onChangeText={setEndTimeStr}
|
||||
placeholder="00:00"
|
||||
placeholderTextColor="rgba(255,255,255,0.3)"
|
||||
keyboardType="numbers-and-punctuation"
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity onPress={handleCaptureEnd} style={localStyles.captureBtn}>
|
||||
<MaterialIcons name="my-location" size={20} color="white" />
|
||||
<Text style={localStyles.captureText}>Capture</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={localStyles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
onPress={onClose}
|
||||
disabled={isSubmitting}
|
||||
style={[localStyles.cancelBtn, isSubmitting && { opacity: 0.5 }]}
|
||||
>
|
||||
<Text style={localStyles.cancelBtnText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
style={[localStyles.submitBtn, isSubmitting && { opacity: 0.7 }]}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<ActivityIndicator color="black" />
|
||||
) : (
|
||||
<>
|
||||
<MaterialIcons name="send" size={18} color="black" />
|
||||
<Text style={localStyles.submitBtnText}>Submit</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const localStyles = StyleSheet.create({
|
||||
centeredView: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 40,
|
||||
},
|
||||
modalContainer: {
|
||||
backgroundColor: 'rgba(20, 20, 20, 0.98)',
|
||||
borderRadius: 28,
|
||||
padding: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 15,
|
||||
elevation: 20,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
title: {
|
||||
color: 'white',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
closeButton: {
|
||||
padding: 4,
|
||||
},
|
||||
content: {
|
||||
gap: 20,
|
||||
},
|
||||
inputRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
gap: 12,
|
||||
},
|
||||
label: {
|
||||
color: 'rgba(255,255,255,0.6)',
|
||||
fontSize: 12,
|
||||
marginBottom: 8,
|
||||
fontWeight: '600',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
},
|
||||
captureBtn: {
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
height: 48,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
captureText: {
|
||||
color: 'white',
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
summaryBox: {
|
||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
marginTop: 8,
|
||||
},
|
||||
summaryText: {
|
||||
color: 'rgba(255,255,255,0.5)',
|
||||
fontSize: 14,
|
||||
marginBottom: 4,
|
||||
},
|
||||
hintText: {
|
||||
color: 'rgba(255,255,255,0.3)',
|
||||
fontSize: 12,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginTop: 12,
|
||||
},
|
||||
cancelBtn: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
borderRadius: 16,
|
||||
height: 56,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
cancelBtnText: {
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
submitBtn: {
|
||||
flex: 2,
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 16,
|
||||
height: 56,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
submitBtnText: {
|
||||
color: 'black',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
|
|
@ -59,6 +59,8 @@ export interface AppSettings {
|
|||
// Playback behavior
|
||||
alwaysResume: boolean; // If true, resume automatically without prompt when progress < 85%
|
||||
skipIntroEnabled: boolean; // Enable/disable Skip Intro overlay (IntroDB)
|
||||
introSubmitEnabled: boolean; // Enable/disable Intro Submission
|
||||
introDbApiKey: string; // API Key for IntroDB submission
|
||||
// Downloads
|
||||
enableDownloads: boolean; // Show Downloads tab and enable saving streams
|
||||
// Theme settings
|
||||
|
|
@ -147,6 +149,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||
// Playback behavior defaults
|
||||
alwaysResume: true,
|
||||
skipIntroEnabled: true,
|
||||
introSubmitEnabled: false,
|
||||
introDbApiKey: '',
|
||||
// Downloads
|
||||
enableDownloads: false,
|
||||
useExternalPlayerForDownloads: false,
|
||||
|
|
|
|||
|
|
@ -417,7 +417,12 @@
|
|||
"timing_offset": "Timing Offset (s)",
|
||||
"visual_sync": "Visual Sync",
|
||||
"timing_hint": "Nudge subtitles earlier (-) or later (+) to sync if needed.",
|
||||
"reset_defaults": "Reset to defaults"
|
||||
"reset_defaults": "Reset to defaults",
|
||||
"mark_intro_start": "Mark Intro Start",
|
||||
"mark_intro_end": "Mark Intro End",
|
||||
"intro_start_marked": "Intro start marked",
|
||||
"intro_submitted": "Intro submitted successfully",
|
||||
"intro_submit_failed": "Failed to submit intro"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
||||
import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions } from 'react-native';
|
||||
import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions, TextInput } from 'react-native';
|
||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
|
@ -13,6 +13,7 @@ import { MaterialIcons } from '@expo/vector-icons';
|
|||
import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SvgXml } from 'react-native-svg';
|
||||
import { toastService } from '../../services/toastService';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -77,6 +78,16 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
|
|||
const config = useRealtimeConfig();
|
||||
|
||||
const [introDbLogoXml, setIntroDbLogoXml] = useState<string | null>(null);
|
||||
const [apiKeyInput, setApiKeyInput] = useState(settings?.introDbApiKey || '');
|
||||
|
||||
useEffect(() => {
|
||||
setApiKeyInput(settings?.introDbApiKey || '');
|
||||
}, [settings?.introDbApiKey]);
|
||||
|
||||
const handleApiKeySubmit = () => {
|
||||
updateSetting('introDbApiKey', apiKeyInput);
|
||||
toastService.success(t('settings.items.api_key_saved', { defaultValue: 'API Key Saved' }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
|
@ -225,6 +236,49 @@ export const PlaybackSettingsContent: React.FC<PlaybackSettingsContentProps> = (
|
|||
/>
|
||||
</SettingsCard>
|
||||
|
||||
{/* IntroDB Contribution Section */}
|
||||
<SettingsCard title={t('settings.sections.introdb_contribution', { defaultValue: 'IntroDB Contribution' })} isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title={t('settings.items.enable_intro_submission', { defaultValue: 'Enable Intro Submission' })}
|
||||
description={t('settings.items.enable_intro_submission_desc', { defaultValue: 'Contribute timestamps to the community' })}
|
||||
icon="flag"
|
||||
renderControl={() => (
|
||||
<CustomSwitch
|
||||
value={settings?.introSubmitEnabled ?? false}
|
||||
onValueChange={(value) => updateSetting('introSubmitEnabled', value)}
|
||||
/>
|
||||
)}
|
||||
isLast={!settings?.introSubmitEnabled}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
|
||||
{settings?.introSubmitEnabled && (
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.inputLabel}>
|
||||
{t('settings.items.introdb_api_key', { defaultValue: 'INTRODB API KEY' })}
|
||||
</Text>
|
||||
<View style={styles.apiKeyRow}>
|
||||
<TextInput
|
||||
style={[styles.input, { flex: 1, marginRight: 10, color: currentTheme.colors.highEmphasis }]}
|
||||
value={apiKeyInput}
|
||||
onChangeText={setApiKeyInput}
|
||||
placeholder="Enter your API key"
|
||||
placeholderTextColor={currentTheme.colors.mediumEmphasis}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
secureTextEntry
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.confirmButton}
|
||||
onPress={handleApiKeySubmit}
|
||||
>
|
||||
<MaterialIcons name="check" size={24} color="black" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</SettingsCard>
|
||||
|
||||
{/* Audio & Subtitle Preferences */}
|
||||
<SettingsCard title={t('settings.sections.audio_subtitles')} isTablet={isTablet}>
|
||||
<SettingItem
|
||||
|
|
@ -542,6 +596,39 @@ const styles = StyleSheet.create({
|
|||
fontSize: 13,
|
||||
color: 'rgba(255,255,255,0.5)',
|
||||
},
|
||||
inputContainer: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 16,
|
||||
paddingTop: 8,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 12,
|
||||
color: 'rgba(255,255,255,0.5)',
|
||||
marginBottom: 8,
|
||||
marginLeft: 4,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
},
|
||||
apiKeyRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
confirmButton: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 12,
|
||||
width: 48,
|
||||
height: 48,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export default PlaybackSettingsScreen;
|
||||
|
|
|
|||
|
|
@ -187,6 +187,44 @@ async function fetchFromIntroDb(imdbId: string, season: number, episode: number)
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits an intro timestamp to IntroDB
|
||||
*/
|
||||
export async function submitIntro(
|
||||
apiKey: string,
|
||||
imdbId: string,
|
||||
season: number,
|
||||
episode: number,
|
||||
startTime: number, // in seconds
|
||||
endTime: number // in seconds
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!apiKey) {
|
||||
logger.warn('[IntroService] Missing API key for submission');
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await axios.post(`${INTRODB_API_URL}/submit`, {
|
||||
imdb_id: imdbId,
|
||||
season,
|
||||
episode,
|
||||
start_ms: Math.round(startTime * 1000),
|
||||
end_ms: Math.round(endTime * 1000),
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
return response.status === 200 || response.status === 201;
|
||||
} catch (error: any) {
|
||||
logger.error('[IntroService] Error submitting intro:', error?.response?.data || error?.message || error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches skip intervals (intro, outro, recap) from available providers
|
||||
*/
|
||||
|
|
@ -266,7 +304,8 @@ export async function getIntroTimestamps(
|
|||
|
||||
export const introService = {
|
||||
getIntroTimestamps,
|
||||
getSkipTimes
|
||||
getSkipTimes,
|
||||
submitIntro
|
||||
};
|
||||
|
||||
export default introService;
|
||||
|
|
|
|||
45
src/types/react-native-background-downloader.d.ts
vendored
Normal file
45
src/types/react-native-background-downloader.d.ts
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
declare module '@kesha-antonov/react-native-background-downloader' {
|
||||
export interface DownloadTask {
|
||||
id: string;
|
||||
percent: number;
|
||||
bytesWritten: number;
|
||||
totalBytes: number;
|
||||
state: 'DOWNLOADING' | 'PAUSED' | 'DONE' | 'STOPPED' | 'FAILED';
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
stop(): void;
|
||||
start(): void;
|
||||
begin(handler: (expectedBytes: number) => void): DownloadTask;
|
||||
progress(handler: (percent: number, bytesWritten: number, totalBytes: number) => void): DownloadTask;
|
||||
done(handler: () => void): DownloadTask;
|
||||
error(handler: (error: any) => void): DownloadTask;
|
||||
}
|
||||
|
||||
export const directories: {
|
||||
documents: string;
|
||||
};
|
||||
|
||||
export function download(options: { id: string; url: string; destination: string; headers?: Record<string, string>; metadata?: any }): DownloadTask;
|
||||
|
||||
export function checkForExistingDownloads(): Promise<DownloadTask[]>;
|
||||
|
||||
// Legacy exports to match library behavior
|
||||
export function completeHandler(id: string): void;
|
||||
|
||||
export function createDownloadTask(options: { id: string; url: string; destination: string; headers?: Record<string, string>; metadata?: any }): DownloadTask;
|
||||
|
||||
export function getExistingDownloadTasks(): Promise<DownloadTask[]>;
|
||||
|
||||
const RNBackgroundDownloader: {
|
||||
download(options: { id: string; url: string; destination: string; headers?: Record<string, string>; metadata?: any }): DownloadTask;
|
||||
checkForExistingDownloads(): Promise<DownloadTask[]>;
|
||||
directories: {
|
||||
documents: string;
|
||||
};
|
||||
completeHandler(id: string): void;
|
||||
createDownloadTask(options: { id: string; url: string; destination: string; headers?: Record<string, string>; metadata?: any }): DownloadTask;
|
||||
getExistingDownloadTasks(): Promise<DownloadTask[]>;
|
||||
};
|
||||
|
||||
export default RNBackgroundDownloader;
|
||||
}
|
||||
Loading…
Reference in a new issue