react-native-video-trim 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -36
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +109 -21
- package/android/src/main/java/com/videotrim/enums/ErrorCode.java +1 -2
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +2 -0
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +12 -9
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +118 -58
- package/android/src/main/res/layout/video_trimmer_view.xml +20 -0
- package/ios/AssetLoader.swift +1 -1
- package/ios/ErrorCode.swift +1 -1
- package/ios/ProgressAlertController.swift +100 -0
- package/ios/VideoTrim.swift +130 -48
- package/ios/VideoTrimmerViewController.swift +130 -27
- package/lib/commonjs/index.js +8 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +9 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/index.d.ts +168 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/index.tsx +176 -3
- package/ios/VideoTrim.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -4
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package com.videotrim.widgets;
|
|
2
2
|
|
|
3
3
|
import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
|
|
4
|
-
import static com.videotrim.utils.VideoTrimmerUtil.DEFAULT_AUDIO_EXTENSION;
|
|
5
4
|
import static com.videotrim.utils.VideoTrimmerUtil.RECYCLER_VIEW_PADDING;
|
|
6
5
|
import static com.videotrim.utils.VideoTrimmerUtil.VIDEO_FRAMES_WIDTH;
|
|
7
6
|
|
|
@@ -9,8 +8,6 @@ import android.content.Context;
|
|
|
9
8
|
import android.content.pm.ActivityInfo;
|
|
10
9
|
import android.content.res.Configuration;
|
|
11
10
|
import android.graphics.Bitmap;
|
|
12
|
-
import android.graphics.Color;
|
|
13
|
-
import android.graphics.drawable.Drawable;
|
|
14
11
|
import android.graphics.drawable.GradientDrawable;
|
|
15
12
|
import android.media.MediaMetadataRetriever;
|
|
16
13
|
import android.media.MediaPlayer;
|
|
@@ -21,10 +18,10 @@ import android.os.VibrationEffect;
|
|
|
21
18
|
import android.os.Vibrator;
|
|
22
19
|
import android.util.AttributeSet;
|
|
23
20
|
import android.util.Log;
|
|
21
|
+
import android.util.TypedValue;
|
|
24
22
|
import android.view.LayoutInflater;
|
|
25
23
|
import android.view.MotionEvent;
|
|
26
24
|
import android.view.View;
|
|
27
|
-
import android.view.ViewGroup;
|
|
28
25
|
import android.widget.FrameLayout;
|
|
29
26
|
import android.widget.ImageView;
|
|
30
27
|
import android.widget.LinearLayout;
|
|
@@ -33,9 +30,9 @@ import android.widget.RelativeLayout;
|
|
|
33
30
|
import android.widget.TextView;
|
|
34
31
|
import android.widget.VideoView;
|
|
35
32
|
|
|
36
|
-
import androidx.
|
|
37
|
-
import androidx.core.content.res.ResourcesCompat;
|
|
33
|
+
import androidx.appcompat.app.AlertDialog;
|
|
38
34
|
|
|
35
|
+
import com.arthenica.ffmpegkit.FFmpegSession;
|
|
39
36
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
40
37
|
import com.facebook.react.bridge.ReadableMap;
|
|
41
38
|
import com.videotrim.R;
|
|
@@ -64,7 +61,6 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
64
61
|
private Uri mSourceUri;
|
|
65
62
|
private VideoTrimListener mOnTrimVideoListener;
|
|
66
63
|
private int mDuration = 0;
|
|
67
|
-
private Boolean mIsPrepared = false;
|
|
68
64
|
private long mMaxDuration = (long) Double.POSITIVE_INFINITY;
|
|
69
65
|
private long mMinDuration = VideoTrimmerUtil.MIN_SHOOT_DURATION;
|
|
70
66
|
|
|
@@ -105,6 +101,15 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
105
101
|
|
|
106
102
|
private String mOutputExt = "mp4";
|
|
107
103
|
private boolean enableHapticFeedback = true;
|
|
104
|
+
private boolean autoplay = false;
|
|
105
|
+
private long jumpToPositionOnLoad = 0;
|
|
106
|
+
private FrameLayout headerView;
|
|
107
|
+
private TextView headerText;
|
|
108
|
+
private FFmpegSession ffmpegSession;
|
|
109
|
+
private boolean alertOnFailToLoad = true;
|
|
110
|
+
private String alertOnFailTitle = "Error";
|
|
111
|
+
private String alertOnFailMessage = "Fail to load media. Possibly invalid file or no network connection";
|
|
112
|
+
private String alertOnFailCloseText = "Close";
|
|
108
113
|
|
|
109
114
|
public VideoTrimmerView(ReactApplicationContext context, ReadableMap config, AttributeSet attrs) {
|
|
110
115
|
this(context, attrs, 0, config);
|
|
@@ -152,6 +157,9 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
152
157
|
cancelBtn = findViewById(R.id.cancelBtn);
|
|
153
158
|
audioBannerView = findViewById(R.id.audioBannerView);
|
|
154
159
|
failToLoadBtn = findViewById(R.id.failToLoadBtn);
|
|
160
|
+
|
|
161
|
+
headerView = findViewById(R.id.headerView);
|
|
162
|
+
headerText = findViewById(R.id.headerText);
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
public void initByURI(final Uri videoURI) {
|
|
@@ -162,18 +170,11 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
162
170
|
mVideoView.requestFocus();
|
|
163
171
|
|
|
164
172
|
mVideoView.setOnPreparedListener(mp -> {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
videoPrepared();
|
|
168
|
-
mIsPrepared = true;
|
|
169
|
-
}
|
|
173
|
+
mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
|
174
|
+
mediaPrepared();
|
|
170
175
|
});
|
|
171
176
|
|
|
172
|
-
mVideoView.setOnErrorListener(
|
|
173
|
-
mediaFailed();
|
|
174
|
-
mOnTrimVideoListener.onError("Error loading video file. Please try again.", ErrorCode.FAIL_TO_LOAD_VIDEO);
|
|
175
|
-
return true;
|
|
176
|
-
});
|
|
177
|
+
mVideoView.setOnErrorListener(this::onFailToLoadMedia);
|
|
177
178
|
|
|
178
179
|
mVideoView.setOnCompletionListener(mp -> mediaCompleted());
|
|
179
180
|
} else {
|
|
@@ -186,17 +187,10 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
186
187
|
try {
|
|
187
188
|
audioPlayer.setDataSource(videoURI.toString());
|
|
188
189
|
audioPlayer.setOnPreparedListener(mp -> {
|
|
189
|
-
|
|
190
|
-
audioPrepared();
|
|
191
|
-
mIsPrepared = true;
|
|
192
|
-
}
|
|
190
|
+
mediaPrepared();
|
|
193
191
|
});
|
|
194
192
|
audioPlayer.setOnCompletionListener(mp -> mediaCompleted());
|
|
195
|
-
audioPlayer.setOnErrorListener(
|
|
196
|
-
mediaFailed();
|
|
197
|
-
mOnTrimVideoListener.onError("Error loading audio file. Please try again.", ErrorCode.FAIL_TO_LOAD_AUDIO);
|
|
198
|
-
return true;
|
|
199
|
-
});
|
|
193
|
+
audioPlayer.setOnErrorListener(this::onFailToLoadMedia);
|
|
200
194
|
|
|
201
195
|
audioPlayer.prepareAsync(); // use prepareAsync to avoid blocking the main thread
|
|
202
196
|
} catch (IOException e) {
|
|
@@ -207,6 +201,25 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
207
201
|
}
|
|
208
202
|
}
|
|
209
203
|
|
|
204
|
+
private boolean onFailToLoadMedia(MediaPlayer mp, int what, int extra) {
|
|
205
|
+
mediaFailed();
|
|
206
|
+
mOnTrimVideoListener.onError("Error loading media file. Please try again.", ErrorCode.FAIL_TO_LOAD_MEDIA);
|
|
207
|
+
if (alertOnFailToLoad) {
|
|
208
|
+
AlertDialog.Builder builder = new AlertDialog.Builder(mContext.getCurrentActivity());
|
|
209
|
+
builder.setMessage(alertOnFailMessage);
|
|
210
|
+
builder.setTitle(alertOnFailTitle);
|
|
211
|
+
builder.setCancelable(false);
|
|
212
|
+
builder.setPositiveButton(alertOnFailCloseText, (dialog, which) -> {
|
|
213
|
+
dialog.cancel();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
AlertDialog alertDialog = builder.create();
|
|
217
|
+
alertDialog.show();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
210
223
|
private void startShootVideoThumbs(final Context context, int totalThumbsCount, long startPosition, long endPosition) {
|
|
211
224
|
mThumbnailContainer.removeAllViews();
|
|
212
225
|
VideoTrimmerUtil.shootVideoThumbInBackground(mediaMetadataRetriever, totalThumbsCount, startPosition, endPosition,
|
|
@@ -225,28 +238,32 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
225
238
|
});
|
|
226
239
|
}
|
|
227
240
|
|
|
228
|
-
private void
|
|
229
|
-
mDuration = mVideoView.getDuration();
|
|
241
|
+
private void mediaPrepared() {
|
|
242
|
+
mDuration = isVideoType ? mVideoView.getDuration() : audioPlayer.getDuration();
|
|
230
243
|
mMaxDuration = Math.min(mMaxDuration, mDuration);
|
|
231
|
-
mediaMetadataRetriever = MediaMetadataUtil.getMediaMetadataRetriever(mSourceUri.toString());
|
|
232
244
|
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
245
|
+
if (isVideoType) {
|
|
246
|
+
mediaMetadataRetriever = MediaMetadataUtil.getMediaMetadataRetriever(mSourceUri.toString());
|
|
247
|
+
if (mediaMetadataRetriever == null) {
|
|
248
|
+
mOnTrimVideoListener.onError("Error when retrieving video info. Please try again.", ErrorCode.FAIL_TO_GET_VIDEO_INFO);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
237
251
|
|
|
238
|
-
|
|
239
|
-
|
|
252
|
+
// take first frame
|
|
253
|
+
Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
|
|
240
254
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
255
|
+
if (bitmap != null) {
|
|
256
|
+
VideoTrimmerUtil.mThumbWidth = VideoTrimmerUtil.THUMB_HEIGHT * bitmap.getWidth() / bitmap.getHeight();
|
|
257
|
+
}
|
|
244
258
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
259
|
+
VideoTrimmerUtil.SCREEN_WIDTH_FULL = this.getScreenWidthInPortraitMode();
|
|
260
|
+
VideoTrimmerUtil.VIDEO_FRAMES_WIDTH = VideoTrimmerUtil.SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2;
|
|
261
|
+
VideoTrimmerUtil.MAX_COUNT_RANGE = Math.max((VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.mThumbWidth), VideoTrimmerUtil.MAX_COUNT_RANGE);
|
|
248
262
|
|
|
249
|
-
|
|
263
|
+
startShootVideoThumbs(mContext, VideoTrimmerUtil.MAX_COUNT_RANGE, 0, mDuration);
|
|
264
|
+
} else {
|
|
265
|
+
|
|
266
|
+
}
|
|
250
267
|
|
|
251
268
|
// Set initial handle positions if mMaxDuration < video duration
|
|
252
269
|
if (mMaxDuration < mDuration) {
|
|
@@ -259,24 +276,16 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
259
276
|
loadingIndicator.setVisibility(View.GONE);
|
|
260
277
|
mPlayView.setVisibility(View.VISIBLE);
|
|
261
278
|
saveBtn.setVisibility(View.VISIBLE);
|
|
262
|
-
}
|
|
263
279
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
280
|
+
if (jumpToPositionOnLoad > 0) {
|
|
281
|
+
seekTo(jumpToPositionOnLoad > mDuration ? mDuration : jumpToPositionOnLoad, true);
|
|
282
|
+
}
|
|
267
283
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
endTime = mMaxDuration;
|
|
271
|
-
} else {
|
|
272
|
-
endTime = mDuration;
|
|
284
|
+
if (autoplay) {
|
|
285
|
+
playOrPause();
|
|
273
286
|
}
|
|
274
287
|
|
|
275
|
-
|
|
276
|
-
loadingIndicator.setVisibility(View.GONE);
|
|
277
|
-
mPlayView.setVisibility(View.VISIBLE);
|
|
278
|
-
saveBtn.setVisibility(View.VISIBLE);
|
|
279
|
-
// mThumbnailContainer.animate().alpha(1f).setDuration(250).start();
|
|
288
|
+
mOnTrimVideoListener.onLoad(mDuration);
|
|
280
289
|
}
|
|
281
290
|
|
|
282
291
|
private void updateGradientColors(int startColor, int endColor) {
|
|
@@ -375,8 +384,8 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
375
384
|
|
|
376
385
|
public void onSaveClicked() {
|
|
377
386
|
onMediaPause();
|
|
378
|
-
VideoTrimmerUtil.trim(
|
|
379
|
-
|
|
387
|
+
ffmpegSession = VideoTrimmerUtil.trim(
|
|
388
|
+
mSourceUri.toString(),
|
|
380
389
|
StorageUtil.getOutputPath(mContext, mOutputExt),
|
|
381
390
|
mDuration,
|
|
382
391
|
startTime,
|
|
@@ -384,6 +393,14 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
384
393
|
mOnTrimVideoListener);
|
|
385
394
|
}
|
|
386
395
|
|
|
396
|
+
public void onCancelTrimClicked() {
|
|
397
|
+
if (ffmpegSession != null) {
|
|
398
|
+
ffmpegSession.cancel();
|
|
399
|
+
} else {
|
|
400
|
+
mOnTrimVideoListener.onCancelTrim();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
387
404
|
private void seekTo(long msec, boolean needUpdateProgress) {
|
|
388
405
|
if (isVideoType) {
|
|
389
406
|
mVideoView.seekTo((int) msec);
|
|
@@ -462,11 +479,54 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
462
479
|
|
|
463
480
|
if (config.hasKey("outputExt")) {
|
|
464
481
|
mOutputExt = config.getString("outputExt");
|
|
482
|
+
} else if (!isVideoType) {
|
|
483
|
+
mOutputExt = "wav";
|
|
465
484
|
}
|
|
466
485
|
|
|
467
486
|
if (config.hasKey("enableHapticFeedback")) {
|
|
468
487
|
enableHapticFeedback = config.getBoolean("enableHapticFeedback");
|
|
469
488
|
}
|
|
489
|
+
|
|
490
|
+
if (config.hasKey("autoplay")) {
|
|
491
|
+
autoplay = config.getBoolean("autoplay");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (config.hasKey("jumpToPositionOnLoad")) {
|
|
495
|
+
jumpToPositionOnLoad = config.getInt("jumpToPositionOnLoad");
|
|
496
|
+
}
|
|
497
|
+
// check if config.getString("headerText") is not empty
|
|
498
|
+
|
|
499
|
+
if (config.hasKey("headerText") && !config.getString("headerText").isEmpty()){
|
|
500
|
+
headerText.setText(config.getString("headerText"));
|
|
501
|
+
|
|
502
|
+
if (config.hasKey("headerTextSize")) {
|
|
503
|
+
int textSize = config.getInt("headerTextSize");
|
|
504
|
+
if (textSize < 0) {
|
|
505
|
+
textSize = 16;
|
|
506
|
+
}
|
|
507
|
+
headerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (config.hasKey("headerTextColor")) {
|
|
511
|
+
headerText.setTextColor(config.getInt("headerTextColor"));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
headerView.setVisibility(View.VISIBLE);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
alertOnFailToLoad = !config.hasKey("alertOnFailToLoad") || config.getBoolean("alertOnFailToLoad");
|
|
518
|
+
|
|
519
|
+
if (config.hasKey("alertOnFailTitle")) {
|
|
520
|
+
alertOnFailTitle = config.getString("alertOnFailTitle");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (config.hasKey("alertOnFailMessage")) {
|
|
524
|
+
alertOnFailMessage = config.getString("alertOnFailMessage");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (config.hasKey("alertOnFailCloseText")) {
|
|
528
|
+
alertOnFailCloseText = config.getString("alertOnFailCloseText");
|
|
529
|
+
}
|
|
470
530
|
}
|
|
471
531
|
|
|
472
532
|
private void startTimingRunnable() {
|
|
@@ -3,11 +3,31 @@
|
|
|
3
3
|
android:layout_width="match_parent"
|
|
4
4
|
android:layout_height="match_parent"
|
|
5
5
|
android:orientation="vertical">
|
|
6
|
+
<FrameLayout
|
|
7
|
+
android:id="@+id/headerView"
|
|
8
|
+
android:layout_width="match_parent"
|
|
9
|
+
android:layout_height="wrap_content"
|
|
10
|
+
android:minHeight="50dp"
|
|
11
|
+
android:background="@android:color/black"
|
|
12
|
+
android:visibility="gone"
|
|
13
|
+
>
|
|
14
|
+
<TextView
|
|
15
|
+
android:id="@+id/headerText"
|
|
16
|
+
android:layout_width="wrap_content"
|
|
17
|
+
android:layout_height="match_parent"
|
|
18
|
+
android:layout_gravity="center"
|
|
19
|
+
android:clickable="true"
|
|
20
|
+
android:focusable="true"
|
|
21
|
+
android:text="Header text"
|
|
22
|
+
android:textColor="@android:color/white"
|
|
23
|
+
android:textSize="16dp" />
|
|
24
|
+
</FrameLayout>
|
|
6
25
|
|
|
7
26
|
<RelativeLayout
|
|
8
27
|
android:layout_width="match_parent"
|
|
9
28
|
android:layout_height="match_parent"
|
|
10
29
|
android:layout_above="@+id/layout"
|
|
30
|
+
android:layout_below="@+id/headerView"
|
|
11
31
|
android:background="@android:color/black"
|
|
12
32
|
android:gravity="center">
|
|
13
33
|
|
package/ios/AssetLoader.swift
CHANGED
package/ios/ErrorCode.swift
CHANGED
|
@@ -9,7 +9,7 @@ import Foundation
|
|
|
9
9
|
|
|
10
10
|
enum ErrorCode: String {
|
|
11
11
|
case trimmingFailed = "TRIMMING_FAILED"
|
|
12
|
-
case
|
|
12
|
+
case failToLoadMedia = "FAIL_TO_LOAD_MEDIA"
|
|
13
13
|
case failToSaveToPhoto = "FAIL_TO_SAVE_TO_PHOTO"
|
|
14
14
|
case failToShare = "FAIL_TO_SHARE"
|
|
15
15
|
case noPhotoPermission = "NO_PHOTO_PERMISSION"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
//
|
|
2
|
+
// ProgressAlertController.swift
|
|
3
|
+
// react-native-video-trim
|
|
4
|
+
//
|
|
5
|
+
// Created by Duc Trung Mai on 8/18/24.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import UIKit
|
|
9
|
+
|
|
10
|
+
class ProgressAlertController: UIViewController {
|
|
11
|
+
var onDismiss: (() -> Void)?
|
|
12
|
+
|
|
13
|
+
private let titleLabel = UILabel()
|
|
14
|
+
private let progressBar = UIProgressView(progressViewStyle: .default)
|
|
15
|
+
private let actionButton = UIButton(type: .system)
|
|
16
|
+
|
|
17
|
+
override func viewDidLoad() {
|
|
18
|
+
super.viewDidLoad()
|
|
19
|
+
|
|
20
|
+
setupBackground()
|
|
21
|
+
setupAlertView()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private func setupBackground() {
|
|
25
|
+
view.backgroundColor = UIColor.black.withAlphaComponent(0.4)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private func setupAlertView() {
|
|
29
|
+
let alertView = UIView()
|
|
30
|
+
alertView.backgroundColor = UIColor(red: 28/255, green: 28/255, blue: 30/255, alpha: 1.0)
|
|
31
|
+
alertView.layer.cornerRadius = 12
|
|
32
|
+
alertView.translatesAutoresizingMaskIntoConstraints = false
|
|
33
|
+
view.addSubview(alertView)
|
|
34
|
+
|
|
35
|
+
// AlertView Constraints
|
|
36
|
+
NSLayoutConstraint.activate([
|
|
37
|
+
alertView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
38
|
+
alertView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
|
39
|
+
alertView.widthAnchor.constraint(equalToConstant: 270)
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
// Title Label
|
|
43
|
+
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
44
|
+
titleLabel.textAlignment = .center
|
|
45
|
+
titleLabel.font = UIFont.systemFont(ofSize: 18)
|
|
46
|
+
titleLabel.numberOfLines = 0
|
|
47
|
+
titleLabel.textColor = .white
|
|
48
|
+
alertView.addSubview(titleLabel)
|
|
49
|
+
|
|
50
|
+
// Progress Bar
|
|
51
|
+
progressBar.translatesAutoresizingMaskIntoConstraints = false
|
|
52
|
+
alertView.addSubview(progressBar)
|
|
53
|
+
|
|
54
|
+
// Action Button
|
|
55
|
+
actionButton.setTitle("Cancel", for: .normal)
|
|
56
|
+
actionButton.setTitleColor(.systemPink, for: .normal)
|
|
57
|
+
actionButton.titleLabel?.font = UIFont.systemFont(ofSize: 16)
|
|
58
|
+
actionButton.addTarget(self, action: #selector(dismissAlert), for: .touchUpInside)
|
|
59
|
+
actionButton.translatesAutoresizingMaskIntoConstraints = false
|
|
60
|
+
actionButton.isHidden = true
|
|
61
|
+
alertView.addSubview(actionButton)
|
|
62
|
+
|
|
63
|
+
// Constraints for titleLabel, progressBar, and actionButton
|
|
64
|
+
NSLayoutConstraint.activate([
|
|
65
|
+
titleLabel.topAnchor.constraint(equalTo: alertView.topAnchor, constant: 16),
|
|
66
|
+
titleLabel.leadingAnchor.constraint(equalTo: alertView.leadingAnchor, constant: 16),
|
|
67
|
+
titleLabel.trailingAnchor.constraint(equalTo: alertView.trailingAnchor, constant: -16),
|
|
68
|
+
|
|
69
|
+
progressBar.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),
|
|
70
|
+
progressBar.leadingAnchor.constraint(equalTo: alertView.leadingAnchor, constant: 16),
|
|
71
|
+
progressBar.trailingAnchor.constraint(equalTo: alertView.trailingAnchor, constant: -16),
|
|
72
|
+
|
|
73
|
+
actionButton.topAnchor.constraint(equalTo: progressBar.bottomAnchor, constant: 16),
|
|
74
|
+
actionButton.bottomAnchor.constraint(equalTo: alertView.bottomAnchor, constant: -16),
|
|
75
|
+
actionButton.centerXAnchor.constraint(equalTo: alertView.centerXAnchor)
|
|
76
|
+
])
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@objc private func dismissAlert() {
|
|
80
|
+
self.onDismiss?()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func setTitle(_ text: String) {
|
|
84
|
+
titleLabel.text = text
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func setCancelTitle(_ text: String) {
|
|
88
|
+
actionButton.setTitle(text, for: .normal)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func setProgress(_ progress: Float) {
|
|
92
|
+
progressBar.setProgress(progress, animated: true)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func showCancelBtn() {
|
|
96
|
+
// Ensure that the button is properly added to the view hierarchy and that the layout has been updated before hiding the button
|
|
97
|
+
view.layoutIfNeeded()
|
|
98
|
+
actionButton.isHidden = false
|
|
99
|
+
}
|
|
100
|
+
}
|