From 3151f8aeefca7dba19371a03cb18e0be11778c56 Mon Sep 17 00:00:00 2001 From: Saramanda9988 <2074730050@qq.com> Date: Tue, 5 May 2026 20:51:12 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat:=20=E4=B8=BA=E8=AF=BE?= =?UTF-8?q?=E8=A1=A8=E8=83=8C=E6=99=AF=E8=AE=BE=E7=BD=AE=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E8=A3=81=E5=89=AA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(pages)/settings/calendar.tsx | 80 +++++++++++++++++++++++++------ package.json | 1 + yarn.lock | 39 +++++---------- 3 files changed, 78 insertions(+), 42 deletions(-) diff --git a/app/(pages)/settings/calendar.tsx b/app/(pages)/settings/calendar.tsx index a1481c6..f36daa3 100644 --- a/app/(pages)/settings/calendar.tsx +++ b/app/(pages)/settings/calendar.tsx @@ -1,6 +1,12 @@ import { Stack } from "expo-router"; import { useMemo, useState } from "react"; -import { Pressable, ScrollView, Switch, Text } from "react-native"; +import { + Pressable, + ScrollView, + Switch, + Text, + useWindowDimensions, +} from "react-native"; import Toast from "react-native-toast-message"; import { BottomSheet } from "@/components/ui/bottom-sheet"; @@ -12,6 +18,7 @@ import { useCourseStore } from "@/store/course"; import { useScheduleStore } from "@/store/schedule"; export default function CalendarSettingsScreen() { + const { width: windowWidth, height: windowHeight } = useWindowDimensions(); const scheme = useColorScheme(); const isDark = scheme === "dark"; const iconColor = Colors[isDark ? "dark" : "light"].icon; @@ -48,19 +55,64 @@ export default function CalendarSettingsScreen() { // 先关闭 BottomSheet,避免 expo-image-picker 在 RN Modal上下文 // 调用 .launch() 触发 unregistered ActivityResultLauncher setShowBgPicker(false); - const ImagePicker = await import("expo-image-picker"); - const { File, Paths } = await import("expo-file-system"); - const result = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ["images"], - quality: 0.8, - }); - if (result.canceled) return; - await deleteOldBg(backgroundImageUri); - const source = new File(result.assets[0].uri); - const dest = new File(Paths.document, `schedule-bg-${Date.now()}.jpg`); - await source.copy(dest); - setBackgroundImageUri(dest.uri); - Toast.show({ type: "success", text1: "背景已设置", position: "bottom" }); + let tempCroppedUri: string | null = null; + try { + const ImagePicker = await import("expo-image-picker"); + const { File, Paths } = await import("expo-file-system"); + const ExpoImageCropTool = ( + await import("@bsky.app/expo-image-crop-tool") + ).default; + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ["images"], + quality: 1, + }); + if (result.canceled) return; + + const asset = result.assets[0]; + const cropped = await ExpoImageCropTool.openCropperAsync({ + imageUri: asset.uri, + shape: "rectangle", + aspectRatio: + windowWidth > 0 && windowHeight > 0 + ? windowWidth / windowHeight + : undefined, + format: "jpeg", + compressImageQuality: 0.85, + cancelButtonText: "取消", + doneButtonText: "完成", + }); + + tempCroppedUri = cropped.path; + const source = new File(cropped.path); + + const dest = new File(Paths.document, `schedule-bg-${Date.now()}.jpg`); + await source.copy(dest); + await deleteOldBg(backgroundImageUri); + + setBackgroundImageUri(dest.uri); + Toast.show({ + type: "success", + text1: "背景已设置", + position: "bottom", + }); + } catch (error) { + if (error instanceof Error && error.message.includes("Crop cancelled")) return; + Toast.show({ + type: "error", + text1: "背景设置失败", + text2: "图片裁剪或保存时出现问题", + position: "bottom", + }); + } finally { + if (tempCroppedUri) { + try { + const { File } = await import("expo-file-system"); + const temp = new File(tempCroppedUri); + if (temp.exists) temp.delete(); + } catch {} + } + } }; const handleRemoveBg = async () => { diff --git a/package.json b/package.json index d4bdc87..ae58d44 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@bacons/apple-targets": "^4.0.6", + "@bsky.app/expo-image-crop-tool": "^0.5.1", "@expo/vector-icons": "^15.0.2", "@preeternal/react-native-cookie-manager": "^6.3.2", "@react-native-assets/slider": "^11.0.12", diff --git a/yarn.lock b/yarn.lock index bcd750b..d67c14f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -813,6 +813,11 @@ debug "^4.3.4" uuid "^8.3.2" +"@bsky.app/expo-image-crop-tool@^0.5.1": + version "0.5.1" + resolved "https://registry.npmmirror.com/@bsky.app/expo-image-crop-tool/-/expo-image-crop-tool-0.5.1.tgz#c3b0ab56917af541ee849765b9336b057f9d885c" + integrity sha512-3O5r0fgS7qQIjcF5tKIweUZkE1knRjA/0jh9TducjBg+eZyBRPHxCEKH+cma0MY5eGilo2P4iCxeNuaEv8wg1g== + "@egjs/hammerjs@^2.0.17": version "2.0.17" resolved "https://mirrors.cloud.tencent.com/npm/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124" @@ -6820,16 +6825,8 @@ strict-uri-encode@^2.0.0: resolved "https://mirrors.cloud.tencent.com/npm/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://mirrors.cloud.tencent.com/npm/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + name string-width-cjs version "4.2.3" resolved "https://mirrors.cloud.tencent.com/npm/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6913,7 +6910,8 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://mirrors.cloud.tencent.com/npm/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6927,13 +6925,6 @@ strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://mirrors.cloud.tencent.com/npm/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.2.0" resolved "https://mirrors.cloud.tencent.com/npm/strip-ansi/-/strip-ansi-7.2.0.tgz#d22a269522836a627af8d04b5c3fd2c7fa3e32e3" @@ -7444,16 +7435,8 @@ word-wrap@^1.2.5: resolved "https://mirrors.cloud.tencent.com/npm/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://mirrors.cloud.tencent.com/npm/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://mirrors.cloud.tencent.com/npm/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 1966018abb402b0a11480dac21f29cb9074df158 Mon Sep 17 00:00:00 2001 From: Saramanda9988 <2074730050@qq.com> Date: Tue, 5 May 2026 21:15:39 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=AF=B9=E5=8F=96=E6=B6=88=E8=A3=81=E5=89=AA=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(pages)/settings/calendar.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/(pages)/settings/calendar.tsx b/app/(pages)/settings/calendar.tsx index f36daa3..8e35371 100644 --- a/app/(pages)/settings/calendar.tsx +++ b/app/(pages)/settings/calendar.tsx @@ -17,6 +17,16 @@ import { useColorScheme } from "@/hooks/use-color-scheme"; import { useCourseStore } from "@/store/course"; import { useScheduleStore } from "@/store/schedule"; +function isCropCancelled(error: unknown) { + const re = /cancell?ed/i; + if (error && typeof error === "object" && "code" in error) { + if (re.test(String((error as { code: unknown }).code))) return true; + } + if (error instanceof Error) return re.test(error.message); + if (typeof error === "string") return re.test(error); + return false; +} + export default function CalendarSettingsScreen() { const { width: windowWidth, height: windowHeight } = useWindowDimensions(); const scheme = useColorScheme(); @@ -97,7 +107,7 @@ export default function CalendarSettingsScreen() { position: "bottom", }); } catch (error) { - if (error instanceof Error && error.message.includes("Crop cancelled")) return; + if (isCropCancelled(error)) return; Toast.show({ type: "error", text1: "背景设置失败",