@gaozh1024/photo-picker 0.1.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 +90 -0
- package/android/build.gradle +19 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/com/gaozh1024/photopicker/PhotoPickerContract.kt +82 -0
- package/android/src/main/java/com/gaozh1024/photopicker/PhotoPickerModule.kt +291 -0
- package/dist/index.d.mts +218 -0
- package/dist/index.d.ts +218 -0
- package/dist/index.js +620 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +568 -0
- package/dist/index.mjs.map +1 -0
- package/expo-module.config.json +6 -0
- package/package.json +62 -0
- package/react-native.config.js +8 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
MEDIA_PICKER_ROUTES: () => MEDIA_PICKER_ROUTES,
|
|
34
|
+
PhotoAlbumScreen: () => PhotoAlbumScreen,
|
|
35
|
+
PhotoCropScreen: () => PhotoCropScreen,
|
|
36
|
+
clearPhotoAlbumCompleteCallback: () => clearPhotoAlbumCompleteCallback,
|
|
37
|
+
clearPickerCache: () => clearPickerCache,
|
|
38
|
+
createCroppedPhotoAlbumItem: () => createCroppedPhotoAlbumItem,
|
|
39
|
+
formatPhotoPickerText: () => formatPhotoPickerText,
|
|
40
|
+
getPhotoAlbumCompleteCallback: () => getPhotoAlbumCompleteCallback,
|
|
41
|
+
isPhotoPickerNativeError: () => isPhotoPickerNativeError,
|
|
42
|
+
mediaPickerColors: () => mediaPickerColors,
|
|
43
|
+
normalizeOpenOptions: () => normalizeOpenOptions,
|
|
44
|
+
normalizePickerMediaType: () => normalizePickerMediaType,
|
|
45
|
+
pickMedia: () => pickMedia,
|
|
46
|
+
registerPhotoAlbumCompleteCallback: () => registerPhotoAlbumCompleteCallback,
|
|
47
|
+
releaseMedia: () => releaseMedia,
|
|
48
|
+
resolvePhotoPickerUiConfig: () => resolvePhotoPickerUiConfig
|
|
49
|
+
});
|
|
50
|
+
module.exports = __toCommonJS(index_exports);
|
|
51
|
+
|
|
52
|
+
// src/constants.ts
|
|
53
|
+
var MEDIA_PICKER_ROUTES = {
|
|
54
|
+
PHOTO_ALBUM: "PhotoAlbum",
|
|
55
|
+
PHOTO_CROP: "PhotoCrop"
|
|
56
|
+
};
|
|
57
|
+
var mediaPickerColors = {
|
|
58
|
+
primary: { 500: "#3b82f6" },
|
|
59
|
+
slate: {
|
|
60
|
+
50: "#f8fafc",
|
|
61
|
+
100: "#f1f5f9",
|
|
62
|
+
200: "#e2e8f0",
|
|
63
|
+
700: "#334155",
|
|
64
|
+
800: "#1e293b",
|
|
65
|
+
900: "#0f172a",
|
|
66
|
+
950: "#020617"
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/screens/PhotoAlbumScreen.tsx
|
|
71
|
+
var import_react = __toESM(require("react"));
|
|
72
|
+
var import_react_native2 = require("react-native");
|
|
73
|
+
|
|
74
|
+
// src/internal/photoPickerCallbackRegistry.ts
|
|
75
|
+
var callbackRegistry = /* @__PURE__ */ new Map();
|
|
76
|
+
function registerPhotoAlbumCompleteCallback(callback) {
|
|
77
|
+
const callbackId = `photo-picker-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
78
|
+
callbackRegistry.set(callbackId, callback);
|
|
79
|
+
return callbackId;
|
|
80
|
+
}
|
|
81
|
+
function getPhotoAlbumCompleteCallback(callbackId) {
|
|
82
|
+
return callbackId ? callbackRegistry.get(callbackId) : void 0;
|
|
83
|
+
}
|
|
84
|
+
function clearPhotoAlbumCompleteCallback(callbackId) {
|
|
85
|
+
if (callbackId) callbackRegistry.delete(callbackId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/native.ts
|
|
89
|
+
var import_react_native = require("react-native");
|
|
90
|
+
var import_expo_modules_core = require("expo-modules-core");
|
|
91
|
+
function getNativeModule() {
|
|
92
|
+
if (import_react_native.Platform.OS !== "android") {
|
|
93
|
+
throw new Error(
|
|
94
|
+
"@gaozh1024/photo-picker currently provides the native system picker on Android only."
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
return (0, import_expo_modules_core.requireNativeModule)("PhotoPickerModule");
|
|
99
|
+
} catch {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"PhotoPickerModule is unavailable. Rebuild the Android app after installing @gaozh1024/photo-picker."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function pickMedia(options = {}) {
|
|
106
|
+
return getNativeModule().pickMedia(options);
|
|
107
|
+
}
|
|
108
|
+
function isPhotoPickerNativeError(error, code) {
|
|
109
|
+
if (!error || typeof error !== "object" || !("code" in error)) return false;
|
|
110
|
+
const errorCode = error.code;
|
|
111
|
+
return (errorCode === "PICKER_BUSY" || errorCode === "PICKER_LAUNCH_FAILED" || errorCode === "PICKER_SELECTION_LIMIT_UNSUPPORTED") && (code === void 0 || errorCode === code);
|
|
112
|
+
}
|
|
113
|
+
function releaseMedia(uris) {
|
|
114
|
+
return getNativeModule().releaseMedia(uris);
|
|
115
|
+
}
|
|
116
|
+
function clearPickerCache() {
|
|
117
|
+
return getNativeModule().clearPickerCache();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/utils/photoPickerFlow.ts
|
|
121
|
+
var DEFAULT_TEXTS = {
|
|
122
|
+
albumTitle: "\u9009\u62E9\u5A92\u4F53",
|
|
123
|
+
cropTitle: "\u88C1\u526A\u56FE\u7247",
|
|
124
|
+
cropConfirmButton: "\u5B8C\u6210",
|
|
125
|
+
cropSavingButton: "\u4FDD\u5B58\u4E2D",
|
|
126
|
+
cropCircleHint: "\u62D6\u52A8\u548C\u7F29\u653E\u56FE\u7247\uFF0C\u4F7F\u4E3B\u4F53\u4F4D\u4E8E\u5706\u5F62\u533A\u57DF\u5185",
|
|
127
|
+
cropRectHint: "\u62D6\u52A8\u548C\u7F29\u653E\u56FE\u7247\uFF0C\u8C03\u6574\u88C1\u526A\u533A\u57DF",
|
|
128
|
+
cropMissingPhoto: "\u672A\u627E\u5230\u53EF\u88C1\u526A\u56FE\u7247",
|
|
129
|
+
durationLimitAlertTitle: "\u63D0\u793A",
|
|
130
|
+
durationLimitAlertMessage: "\u89C6\u9891\u65F6\u957F\u4E0D\u80FD\u8D85\u8FC7 {maxDuration} \u79D2",
|
|
131
|
+
openAlbumError: "\u6253\u5F00\u7CFB\u7EDF\u9009\u62E9\u5668\u5931\u8D25"
|
|
132
|
+
};
|
|
133
|
+
function normalizeOpenOptions(options) {
|
|
134
|
+
const requestedMaxSelection = options?.maxSelection ?? 9;
|
|
135
|
+
const maxSelection = Number.isFinite(requestedMaxSelection) ? Math.max(1, Math.floor(requestedMaxSelection)) : 1;
|
|
136
|
+
const crop = options?.crop;
|
|
137
|
+
const allowsMultipleSelection = crop ? false : options?.allowsMultipleSelection ?? maxSelection > 1;
|
|
138
|
+
return {
|
|
139
|
+
...options,
|
|
140
|
+
crop,
|
|
141
|
+
maxSelection: crop ? 1 : allowsMultipleSelection ? maxSelection : 1,
|
|
142
|
+
mediaType: crop ? "photo" : options?.mediaType ?? "all",
|
|
143
|
+
allowsMultipleSelection
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function resolvePhotoPickerUiConfig(uiConfig) {
|
|
147
|
+
return {
|
|
148
|
+
texts: {
|
|
149
|
+
...DEFAULT_TEXTS,
|
|
150
|
+
...uiConfig?.texts
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function formatPhotoPickerText(template, variables) {
|
|
155
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => String(variables[key] ?? ""));
|
|
156
|
+
}
|
|
157
|
+
function createCroppedPhotoAlbumItem(photo, manipulated, crop) {
|
|
158
|
+
const baseName = photo.filename ?? photo.fileName ?? "photo";
|
|
159
|
+
const croppedName = `${baseName.replace(/\.[^.]+$/, "")}-cropped.jpg`;
|
|
160
|
+
return {
|
|
161
|
+
...photo,
|
|
162
|
+
id: `${photo.id}-crop-${Date.now()}`,
|
|
163
|
+
uri: manipulated.uri,
|
|
164
|
+
localUri: manipulated.uri,
|
|
165
|
+
filename: croppedName,
|
|
166
|
+
fileName: croppedName,
|
|
167
|
+
mimeType: "image/jpeg",
|
|
168
|
+
width: manipulated.width,
|
|
169
|
+
height: manipulated.height,
|
|
170
|
+
edited: true,
|
|
171
|
+
crop: {
|
|
172
|
+
...crop,
|
|
173
|
+
width: manipulated.width,
|
|
174
|
+
height: manipulated.height
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function normalizePickerMediaType(mediaType) {
|
|
179
|
+
return mediaType === "photo" || mediaType === "video" || mediaType === "all" ? mediaType : "all";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/screens/PhotoAlbumScreen.tsx
|
|
183
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
184
|
+
function PhotoAlbumScreen({ route, navigation }) {
|
|
185
|
+
const startedRef = import_react.default.useRef(false);
|
|
186
|
+
const [errorMessage, setErrorMessage] = import_react.default.useState();
|
|
187
|
+
const callbackId = route?.params?.callbackId;
|
|
188
|
+
const options = import_react.default.useMemo(
|
|
189
|
+
() => normalizeOpenOptions({
|
|
190
|
+
...route?.params?.options,
|
|
191
|
+
maxSelection: route?.params?.options?.maxSelection ?? route?.params?.maxSelection,
|
|
192
|
+
allowsMultipleSelection: route?.params?.options?.allowsMultipleSelection ?? route?.params?.allowsMultipleSelection,
|
|
193
|
+
mediaType: route?.params?.options?.mediaType ?? (route?.params?.mediaTypes?.length === 1 ? route.params.mediaTypes[0] : "all")
|
|
194
|
+
}),
|
|
195
|
+
[
|
|
196
|
+
route?.params?.allowsMultipleSelection,
|
|
197
|
+
route?.params?.maxSelection,
|
|
198
|
+
route?.params?.mediaTypes,
|
|
199
|
+
route?.params?.options
|
|
200
|
+
]
|
|
201
|
+
);
|
|
202
|
+
const uiConfig = import_react.default.useMemo(
|
|
203
|
+
() => resolvePhotoPickerUiConfig({
|
|
204
|
+
...route?.params?.uiConfig,
|
|
205
|
+
...options.uiConfig,
|
|
206
|
+
texts: {
|
|
207
|
+
...route?.params?.uiConfig?.texts,
|
|
208
|
+
...options.uiConfig?.texts
|
|
209
|
+
}
|
|
210
|
+
}),
|
|
211
|
+
[options.uiConfig, route?.params?.uiConfig]
|
|
212
|
+
);
|
|
213
|
+
const routeNames = import_react.default.useMemo(
|
|
214
|
+
() => ({
|
|
215
|
+
photoAlbum: route?.params?.routeNames?.photoAlbum ?? MEDIA_PICKER_ROUTES.PHOTO_ALBUM,
|
|
216
|
+
photoCrop: route?.params?.routeNames?.photoCrop ?? MEDIA_PICKER_ROUTES.PHOTO_CROP
|
|
217
|
+
}),
|
|
218
|
+
[route?.params?.routeNames?.photoAlbum, route?.params?.routeNames?.photoCrop]
|
|
219
|
+
);
|
|
220
|
+
const close = import_react.default.useCallback(() => {
|
|
221
|
+
clearPhotoAlbumCompleteCallback(callbackId);
|
|
222
|
+
navigation?.goBack();
|
|
223
|
+
}, [callbackId, navigation]);
|
|
224
|
+
const complete = import_react.default.useCallback(
|
|
225
|
+
(assets) => {
|
|
226
|
+
const overLimitVideo = options.maxVideoDuration == null ? void 0 : assets.find(
|
|
227
|
+
(asset) => asset.mediaType === "video" && (asset.duration ?? 0) > options.maxVideoDuration
|
|
228
|
+
);
|
|
229
|
+
if (overLimitVideo) {
|
|
230
|
+
import_react_native2.Alert.alert(
|
|
231
|
+
uiConfig.texts.durationLimitAlertTitle,
|
|
232
|
+
formatPhotoPickerText(uiConfig.texts.durationLimitAlertMessage, {
|
|
233
|
+
maxDuration: options.maxVideoDuration
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (options.crop) {
|
|
239
|
+
const photo = assets[0];
|
|
240
|
+
if (!photo) return;
|
|
241
|
+
navigation?.navigate?.(routeNames.photoCrop, {
|
|
242
|
+
photo,
|
|
243
|
+
crop: options.crop,
|
|
244
|
+
quality: options.quality,
|
|
245
|
+
callbackId,
|
|
246
|
+
routeNames,
|
|
247
|
+
uiConfig: options.uiConfig
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
getPhotoAlbumCompleteCallback(callbackId)?.(assets);
|
|
252
|
+
clearPhotoAlbumCompleteCallback(callbackId);
|
|
253
|
+
navigation?.goBack();
|
|
254
|
+
},
|
|
255
|
+
[
|
|
256
|
+
callbackId,
|
|
257
|
+
navigation,
|
|
258
|
+
options,
|
|
259
|
+
routeNames,
|
|
260
|
+
uiConfig.texts.durationLimitAlertMessage,
|
|
261
|
+
uiConfig.texts.durationLimitAlertTitle
|
|
262
|
+
]
|
|
263
|
+
);
|
|
264
|
+
const openPicker = import_react.default.useCallback(async () => {
|
|
265
|
+
setErrorMessage(void 0);
|
|
266
|
+
try {
|
|
267
|
+
const result = await pickMedia({
|
|
268
|
+
mediaType: options.mediaType,
|
|
269
|
+
maxSelection: options.maxSelection,
|
|
270
|
+
allowsMultipleSelection: options.allowsMultipleSelection,
|
|
271
|
+
cacheMode: "copy"
|
|
272
|
+
});
|
|
273
|
+
if (result.cancelled) {
|
|
274
|
+
close();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
complete(result.assets);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
setErrorMessage(error instanceof Error ? error.message : uiConfig.texts.openAlbumError);
|
|
280
|
+
}
|
|
281
|
+
}, [
|
|
282
|
+
close,
|
|
283
|
+
complete,
|
|
284
|
+
options.allowsMultipleSelection,
|
|
285
|
+
options.maxSelection,
|
|
286
|
+
options.mediaType,
|
|
287
|
+
uiConfig.texts.openAlbumError
|
|
288
|
+
]);
|
|
289
|
+
import_react.default.useEffect(() => {
|
|
290
|
+
if (startedRef.current) return;
|
|
291
|
+
startedRef.current = true;
|
|
292
|
+
void openPicker();
|
|
293
|
+
}, [openPicker]);
|
|
294
|
+
import_react.default.useEffect(
|
|
295
|
+
() => () => {
|
|
296
|
+
if (!options.crop) {
|
|
297
|
+
clearPhotoAlbumCompleteCallback(callbackId);
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
[callbackId, options.crop]
|
|
301
|
+
);
|
|
302
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: styles.container, children: [
|
|
303
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.ActivityIndicator, { size: "large", color: "#3b82f6" }),
|
|
304
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: styles.title, children: errorMessage ? uiConfig.texts.openAlbumError : uiConfig.texts.albumTitle }),
|
|
305
|
+
errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: styles.message, children: errorMessage }) : null,
|
|
306
|
+
errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: styles.actions, children: [
|
|
307
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Pressable, { onPress: () => void openPicker(), style: styles.primaryButton, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: styles.primaryButtonText, children: "\u91CD\u8BD5" }) }),
|
|
308
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Pressable, { onPress: close, style: styles.secondaryButton, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: styles.secondaryButtonText, children: "\u53D6\u6D88" }) })
|
|
309
|
+
] }) : null
|
|
310
|
+
] });
|
|
311
|
+
}
|
|
312
|
+
var styles = import_react_native2.StyleSheet.create({
|
|
313
|
+
container: {
|
|
314
|
+
flex: 1,
|
|
315
|
+
alignItems: "center",
|
|
316
|
+
justifyContent: "center",
|
|
317
|
+
padding: 24,
|
|
318
|
+
backgroundColor: "#f8fafc"
|
|
319
|
+
},
|
|
320
|
+
title: { marginTop: 16, color: "#0f172a", fontSize: 16, fontWeight: "600" },
|
|
321
|
+
message: { marginTop: 8, color: "#64748b", fontSize: 13, lineHeight: 20, textAlign: "center" },
|
|
322
|
+
actions: { flexDirection: "row", gap: 12, marginTop: 20 },
|
|
323
|
+
primaryButton: {
|
|
324
|
+
borderRadius: 8,
|
|
325
|
+
backgroundColor: "#2563eb",
|
|
326
|
+
paddingHorizontal: 18,
|
|
327
|
+
paddingVertical: 10
|
|
328
|
+
},
|
|
329
|
+
primaryButtonText: { color: "#ffffff", fontSize: 14, fontWeight: "600" },
|
|
330
|
+
secondaryButton: {
|
|
331
|
+
borderRadius: 8,
|
|
332
|
+
borderWidth: 1,
|
|
333
|
+
borderColor: "#cbd5e1",
|
|
334
|
+
paddingHorizontal: 18,
|
|
335
|
+
paddingVertical: 10
|
|
336
|
+
},
|
|
337
|
+
secondaryButtonText: { color: "#334155", fontSize: 14, fontWeight: "600" }
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// src/screens/PhotoCropScreen.tsx
|
|
341
|
+
var import_react2 = __toESM(require("react"));
|
|
342
|
+
var import_react_native3 = require("react-native");
|
|
343
|
+
var import_native2 = require("@react-navigation/native");
|
|
344
|
+
var import_react_native_zoom_toolkit = require("react-native-zoom-toolkit");
|
|
345
|
+
var import_expo_image = require("expo-image");
|
|
346
|
+
var import_expo_image_manipulator = require("expo-image-manipulator");
|
|
347
|
+
var import_react_native_safe_area_context = require("react-native-safe-area-context");
|
|
348
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
349
|
+
var { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = import_react_native3.Dimensions.get("window");
|
|
350
|
+
var HEADER_ROW_HEIGHT = 48;
|
|
351
|
+
var FOOTER_MIN_HEIGHT = 52;
|
|
352
|
+
var CONTENT_HORIZONTAL_PADDING = 16;
|
|
353
|
+
var CONTENT_VERTICAL_PADDING = 12;
|
|
354
|
+
function getCropBoxSize(aspect, maxWidth, maxHeight) {
|
|
355
|
+
const ratio = aspect[0] / aspect[1];
|
|
356
|
+
let width = maxWidth;
|
|
357
|
+
let height = width / ratio;
|
|
358
|
+
if (height > maxHeight) {
|
|
359
|
+
height = maxHeight;
|
|
360
|
+
width = height * ratio;
|
|
361
|
+
}
|
|
362
|
+
return { width: Math.floor(width), height: Math.floor(height) };
|
|
363
|
+
}
|
|
364
|
+
function getImageDisplaySize(imageWidth, imageHeight, cropWidth, cropHeight) {
|
|
365
|
+
const imageRatio = imageWidth / imageHeight;
|
|
366
|
+
const cropRatio = cropWidth / cropHeight;
|
|
367
|
+
return imageRatio > cropRatio ? { width: cropHeight * imageRatio, height: cropHeight } : { width: cropWidth, height: cropWidth / imageRatio };
|
|
368
|
+
}
|
|
369
|
+
function PhotoCropScreen({ route, navigation }) {
|
|
370
|
+
const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
|
|
371
|
+
const isFocused = (0, import_native2.useIsFocused)();
|
|
372
|
+
const cropRef = import_react2.default.useRef(null);
|
|
373
|
+
const [saving, setSaving] = import_react2.default.useState(false);
|
|
374
|
+
const [closing, setClosing] = import_react2.default.useState(false);
|
|
375
|
+
const [contentLayout, setContentLayout] = import_react2.default.useState({ width: 0, height: 0 });
|
|
376
|
+
const photo = route?.params?.photo;
|
|
377
|
+
const cropOptions = route?.params?.crop;
|
|
378
|
+
const callbackId = route?.params?.callbackId;
|
|
379
|
+
const uiConfig = import_react2.default.useMemo(
|
|
380
|
+
() => resolvePhotoPickerUiConfig(route?.params?.uiConfig),
|
|
381
|
+
[route?.params?.uiConfig]
|
|
382
|
+
);
|
|
383
|
+
const quality = route?.params?.quality ?? cropOptions?.quality ?? 1;
|
|
384
|
+
const aspect = cropOptions?.aspect ?? [1, 1];
|
|
385
|
+
const isCircleCrop = cropOptions?.shape === "circle";
|
|
386
|
+
const headerHeight = insets.top + HEADER_ROW_HEIGHT;
|
|
387
|
+
const footerHeight = Math.max(insets.bottom, 16) + FOOTER_MIN_HEIGHT;
|
|
388
|
+
const cropBox = import_react2.default.useMemo(
|
|
389
|
+
() => getCropBoxSize(
|
|
390
|
+
aspect,
|
|
391
|
+
SCREEN_WIDTH - CONTENT_HORIZONTAL_PADDING * 2,
|
|
392
|
+
SCREEN_HEIGHT - headerHeight - footerHeight - CONTENT_VERTICAL_PADDING * 2
|
|
393
|
+
),
|
|
394
|
+
[aspect, footerHeight, headerHeight]
|
|
395
|
+
);
|
|
396
|
+
const cropFramePosition = import_react2.default.useMemo(
|
|
397
|
+
() => ({
|
|
398
|
+
left: Math.max(0, (contentLayout.width - cropBox.width) / 2),
|
|
399
|
+
top: Math.max(0, (contentLayout.height - cropBox.height) / 2)
|
|
400
|
+
}),
|
|
401
|
+
[contentLayout.height, contentLayout.width, cropBox.height, cropBox.width]
|
|
402
|
+
);
|
|
403
|
+
const imageSize = import_react2.default.useMemo(
|
|
404
|
+
() => !photo ? { width: cropBox.width, height: cropBox.height } : getImageDisplaySize(
|
|
405
|
+
Math.max(photo.width, 1),
|
|
406
|
+
Math.max(photo.height, 1),
|
|
407
|
+
cropBox.width,
|
|
408
|
+
cropBox.height
|
|
409
|
+
),
|
|
410
|
+
[cropBox.height, cropBox.width, photo]
|
|
411
|
+
);
|
|
412
|
+
const closeFlow = import_react2.default.useCallback(
|
|
413
|
+
(count = 1) => {
|
|
414
|
+
if (closing) return;
|
|
415
|
+
setClosing(true);
|
|
416
|
+
requestAnimationFrame(() => {
|
|
417
|
+
if (count > 1 && navigation?.pop) navigation.pop(count);
|
|
418
|
+
else navigation?.goBack();
|
|
419
|
+
});
|
|
420
|
+
},
|
|
421
|
+
[closing, navigation]
|
|
422
|
+
);
|
|
423
|
+
const handleCancel = import_react2.default.useCallback(() => {
|
|
424
|
+
clearPhotoAlbumCompleteCallback(callbackId);
|
|
425
|
+
closeFlow();
|
|
426
|
+
}, [callbackId, closeFlow]);
|
|
427
|
+
const handleConfirm = import_react2.default.useCallback(async () => {
|
|
428
|
+
if (!photo || !cropRef.current || saving) return;
|
|
429
|
+
setSaving(true);
|
|
430
|
+
try {
|
|
431
|
+
const result = cropRef.current.crop();
|
|
432
|
+
const manipulated = await (0, import_expo_image_manipulator.manipulateAsync)(photo.uri, [{ crop: result.crop }], {
|
|
433
|
+
compress: quality,
|
|
434
|
+
format: import_expo_image_manipulator.SaveFormat.JPEG
|
|
435
|
+
});
|
|
436
|
+
const croppedPhoto = createCroppedPhotoAlbumItem(photo, manipulated, cropOptions);
|
|
437
|
+
getPhotoAlbumCompleteCallback(callbackId)?.([croppedPhoto]);
|
|
438
|
+
clearPhotoAlbumCompleteCallback(callbackId);
|
|
439
|
+
closeFlow(2);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
console.error("[photo-picker] crop failed", error);
|
|
442
|
+
} finally {
|
|
443
|
+
setSaving(false);
|
|
444
|
+
}
|
|
445
|
+
}, [callbackId, closeFlow, cropOptions, photo, quality, saving]);
|
|
446
|
+
if (!photo) {
|
|
447
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { style: styles2.empty, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: styles2.emptyText, children: uiConfig.texts.cropMissingPhoto }) });
|
|
448
|
+
}
|
|
449
|
+
const footerHint = isCircleCrop ? uiConfig.texts.cropCircleHint : uiConfig.texts.cropRectHint;
|
|
450
|
+
const showContent = isFocused && !closing;
|
|
451
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: styles2.screen, children: [
|
|
452
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: [styles2.header, { height: headerHeight, paddingTop: insets.top }], children: [
|
|
453
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Pressable, { onPress: handleCancel, style: styles2.headerButton, accessibilityRole: "button", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: styles2.headerIcon, children: "\xD7" }) }),
|
|
454
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: styles2.headerTitle, children: uiConfig.texts.cropTitle }),
|
|
455
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
456
|
+
import_react_native3.Pressable,
|
|
457
|
+
{
|
|
458
|
+
onPress: handleConfirm,
|
|
459
|
+
style: styles2.headerButton,
|
|
460
|
+
disabled: saving,
|
|
461
|
+
accessibilityRole: "button",
|
|
462
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: styles2.headerAction, children: saving ? uiConfig.texts.cropSavingButton : uiConfig.texts.cropConfirmButton })
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
] }),
|
|
466
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
467
|
+
import_react_native3.View,
|
|
468
|
+
{
|
|
469
|
+
style: styles2.content,
|
|
470
|
+
onLayout: ({ nativeEvent }) => setContentLayout(nativeEvent.layout),
|
|
471
|
+
children: [
|
|
472
|
+
showContent ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
473
|
+
import_react_native_zoom_toolkit.CropZoom,
|
|
474
|
+
{
|
|
475
|
+
ref: cropRef,
|
|
476
|
+
cropSize: cropBox,
|
|
477
|
+
resolution: { width: Math.max(photo.width, 1), height: Math.max(photo.height, 1) },
|
|
478
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
479
|
+
import_expo_image.Image,
|
|
480
|
+
{
|
|
481
|
+
source: { uri: photo.uri },
|
|
482
|
+
style: imageSize,
|
|
483
|
+
contentFit: "cover",
|
|
484
|
+
transition: 0,
|
|
485
|
+
cachePolicy: "memory-disk"
|
|
486
|
+
}
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
) : null,
|
|
490
|
+
showContent && contentLayout.width > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { pointerEvents: "none", style: styles2.overlay, children: [
|
|
491
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
492
|
+
import_react_native3.View,
|
|
493
|
+
{
|
|
494
|
+
style: [styles2.mask, { left: 0, top: 0, right: 0, height: cropFramePosition.top }]
|
|
495
|
+
}
|
|
496
|
+
),
|
|
497
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
498
|
+
import_react_native3.View,
|
|
499
|
+
{
|
|
500
|
+
style: [
|
|
501
|
+
styles2.mask,
|
|
502
|
+
{
|
|
503
|
+
left: 0,
|
|
504
|
+
top: cropFramePosition.top,
|
|
505
|
+
width: cropFramePosition.left,
|
|
506
|
+
height: cropBox.height
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
}
|
|
510
|
+
),
|
|
511
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
512
|
+
import_react_native3.View,
|
|
513
|
+
{
|
|
514
|
+
style: [
|
|
515
|
+
styles2.mask,
|
|
516
|
+
{
|
|
517
|
+
left: cropFramePosition.left + cropBox.width,
|
|
518
|
+
top: cropFramePosition.top,
|
|
519
|
+
right: 0,
|
|
520
|
+
height: cropBox.height
|
|
521
|
+
}
|
|
522
|
+
]
|
|
523
|
+
}
|
|
524
|
+
),
|
|
525
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
526
|
+
import_react_native3.View,
|
|
527
|
+
{
|
|
528
|
+
style: [
|
|
529
|
+
styles2.mask,
|
|
530
|
+
{ left: 0, right: 0, top: cropFramePosition.top + cropBox.height, bottom: 0 }
|
|
531
|
+
]
|
|
532
|
+
}
|
|
533
|
+
),
|
|
534
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
535
|
+
import_react_native3.View,
|
|
536
|
+
{
|
|
537
|
+
style: [
|
|
538
|
+
styles2.cropFrame,
|
|
539
|
+
{
|
|
540
|
+
width: cropBox.width,
|
|
541
|
+
height: cropBox.height,
|
|
542
|
+
left: cropFramePosition.left,
|
|
543
|
+
top: cropFramePosition.top,
|
|
544
|
+
borderRadius: isCircleCrop ? cropBox.width / 2 : 16
|
|
545
|
+
}
|
|
546
|
+
]
|
|
547
|
+
}
|
|
548
|
+
)
|
|
549
|
+
] }) : null
|
|
550
|
+
]
|
|
551
|
+
}
|
|
552
|
+
),
|
|
553
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
554
|
+
import_react_native3.View,
|
|
555
|
+
{
|
|
556
|
+
style: [
|
|
557
|
+
styles2.footer,
|
|
558
|
+
{ minHeight: footerHeight, paddingBottom: Math.max(insets.bottom, 16) }
|
|
559
|
+
],
|
|
560
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: styles2.hint, children: footerHint })
|
|
561
|
+
}
|
|
562
|
+
)
|
|
563
|
+
] });
|
|
564
|
+
}
|
|
565
|
+
var styles2 = import_react_native3.StyleSheet.create({
|
|
566
|
+
screen: { flex: 1, backgroundColor: "#000000" },
|
|
567
|
+
empty: { flex: 1, justifyContent: "center", alignItems: "center" },
|
|
568
|
+
emptyText: { color: "#334155", fontSize: 16 },
|
|
569
|
+
header: {
|
|
570
|
+
flexDirection: "row",
|
|
571
|
+
alignItems: "center",
|
|
572
|
+
justifyContent: "space-between",
|
|
573
|
+
paddingHorizontal: 12,
|
|
574
|
+
backgroundColor: "#000000"
|
|
575
|
+
},
|
|
576
|
+
headerButton: { minWidth: 52, height: 40, justifyContent: "center", alignItems: "center" },
|
|
577
|
+
headerTitle: { color: "#ffffff", fontSize: 18, fontWeight: "600" },
|
|
578
|
+
headerIcon: { color: "#ffffff", fontSize: 30, lineHeight: 32 },
|
|
579
|
+
headerAction: { color: "#ffffff", fontSize: 15, fontWeight: "600" },
|
|
580
|
+
content: {
|
|
581
|
+
flex: 1,
|
|
582
|
+
justifyContent: "center",
|
|
583
|
+
alignItems: "center",
|
|
584
|
+
overflow: "hidden",
|
|
585
|
+
paddingHorizontal: CONTENT_HORIZONTAL_PADDING,
|
|
586
|
+
paddingVertical: CONTENT_VERTICAL_PADDING,
|
|
587
|
+
backgroundColor: "#000000"
|
|
588
|
+
},
|
|
589
|
+
overlay: { ...import_react_native3.StyleSheet.absoluteFillObject },
|
|
590
|
+
mask: { position: "absolute", backgroundColor: "rgba(0,0,0,0.58)" },
|
|
591
|
+
cropFrame: { position: "absolute", borderWidth: 2, borderColor: "#ffffff" },
|
|
592
|
+
footer: {
|
|
593
|
+
alignItems: "center",
|
|
594
|
+
justifyContent: "center",
|
|
595
|
+
paddingHorizontal: 16,
|
|
596
|
+
paddingTop: 12,
|
|
597
|
+
backgroundColor: "#000000"
|
|
598
|
+
},
|
|
599
|
+
hint: { color: "rgba(255,255,255,0.8)" }
|
|
600
|
+
});
|
|
601
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
602
|
+
0 && (module.exports = {
|
|
603
|
+
MEDIA_PICKER_ROUTES,
|
|
604
|
+
PhotoAlbumScreen,
|
|
605
|
+
PhotoCropScreen,
|
|
606
|
+
clearPhotoAlbumCompleteCallback,
|
|
607
|
+
clearPickerCache,
|
|
608
|
+
createCroppedPhotoAlbumItem,
|
|
609
|
+
formatPhotoPickerText,
|
|
610
|
+
getPhotoAlbumCompleteCallback,
|
|
611
|
+
isPhotoPickerNativeError,
|
|
612
|
+
mediaPickerColors,
|
|
613
|
+
normalizeOpenOptions,
|
|
614
|
+
normalizePickerMediaType,
|
|
615
|
+
pickMedia,
|
|
616
|
+
registerPhotoAlbumCompleteCallback,
|
|
617
|
+
releaseMedia,
|
|
618
|
+
resolvePhotoPickerUiConfig
|
|
619
|
+
});
|
|
620
|
+
//# sourceMappingURL=index.js.map
|