Ipad适配问题

This commit is contained in:
吕新雨
2026-03-03 19:55:53 +08:00
parent 0ad21da246
commit d37262876b
20 changed files with 692 additions and 147 deletions

View File

@@ -2,13 +2,13 @@ import { useEffect, useLayoutEffect, useMemo, useState, useCallback, useRef } fr
import {
StyleSheet,
View,
Dimensions,
Text,
Pressable,
PanResponder,
Animated as RNAnimated,
ImageBackground,
Platform,
useWindowDimensions,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { useFocusEffect } from 'expo-router';
@@ -57,8 +57,6 @@ import { wrapText } from '@/src/features/textWrap';
import { defaultMeasureWidthImpl } from '@/src/features/textWrap/measure';
import { ensureDailyWidgetRecoUpToDate } from '@/src/modules/dailyWidgetReco';
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
// 预定义风景图列表
const NATURE_IMAGES = [
require('@/assets/theme/nature/1.png'),
@@ -97,7 +95,9 @@ type FeedItem = { content_id: string; text: string };
export default function HomeScreen() {
const { t, i18n } = useTranslation();
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const isEnglish = i18n.language?.startsWith('en');
const isTablet = Platform.OS === 'ios' && Math.min(windowWidth, windowHeight) >= 768;
const recoLang: 'en' | 'tc' = toBackendLocaleFromLanguageTag(i18n.language);
const insets = useSafeAreaInsets();
const [index, setIndex] = useState(0);
@@ -248,8 +248,8 @@ export default function HomeScreen() {
});
const fontSpec = {
fontSize: 22,
fontWeight: lang === 'EN' ? '600' : '700',
fontSize: 24,
fontWeight: lang === 'EN' ? '700' : '800',
fontFamily: String(fontFamily ?? 'System'),
};
@@ -659,6 +659,10 @@ export default function HomeScreen() {
}
}
const actionsBottom = isTablet
? Math.max(insets.bottom + 36, Math.min(windowHeight * 0.12, 140))
: windowHeight * 0.16;
return (
<View style={[styles.container, { backgroundColor }]} {...panResponder.panHandlers}>
{themeMode === 'scenery' && (
@@ -670,7 +674,15 @@ export default function HomeScreen() {
)}
{/* 自绘顶部按钮:不使用系统 Header彻底避免 iOS 导航栏的毛玻璃/液玻璃材质 */}
<View style={[styles.topRight, { top: insets.top + 8 }]}>
<View
style={[
styles.topRight,
{
top: insets.top + (isTablet ? 16 : 8),
right: isTablet ? 26 : 20,
},
]}
>
<CircleIconButton
onPress={() => setThemeOpen(true)}
accessibilityLabel={t('home.theme')}
@@ -685,19 +697,21 @@ export default function HomeScreen() {
</CircleIconButton>
</View>
<Animated.View
style={[styles.card, textAnimatedStyle, themeMode === 'scenery' && styles.sceneryCard]}
onLayout={(e) => {
const w = e.nativeEvent.layout.width;
if (Number.isFinite(w) && w > 0) setCardWidth(w);
}}
>
<Text style={[styles.text, isEnglish && styles.textEnglish, themeMode === 'scenery' && styles.sceneryText]}>
{wrappedText || item.text}
</Text>
<Animated.View style={[styles.card, textAnimatedStyle, themeMode === 'scenery' && styles.sceneryCard]}>
<View
style={[styles.textMeasureBox, isTablet && styles.textMeasureBoxTablet]}
onLayout={(e) => {
const w = e.nativeEvent.layout.width;
if (Number.isFinite(w) && w > 0) setCardWidth(w);
}}
>
<Text style={[styles.text, isEnglish && styles.textEnglish, themeMode === 'scenery' && styles.sceneryText]}>
{wrappedText || item.text}
</Text>
</View>
</Animated.View>
<View style={styles.actions}>
<View style={[styles.actions, { bottom: actionsBottom }]}>
<Animated.View style={[styles.reactionButton, likeAnimatedStyle]}>
<Pressable
onPress={onPressLike}
@@ -762,7 +776,6 @@ const styles = StyleSheet.create({
},
topRight: {
position: 'absolute',
right: 20,
flexDirection: 'row',
gap: 10,
zIndex: 30,
@@ -787,16 +800,23 @@ const styles = StyleSheet.create({
zIndex: 5, // 降低层级,防止遮挡底部按钮
},
text: {
fontSize: 22,
lineHeight: 32,
fontSize: 24,
lineHeight: 34,
color: '#5E2A28',
fontWeight: '700',
fontWeight: '800',
textAlign: 'center',
},
textMeasureBox: {
width: '100%',
alignItems: 'center',
},
textMeasureBoxTablet: {
maxWidth: 760,
},
textEnglish: {
fontFamily: 'STIXTwoText',
// 英文字体观感更细一点,避免过粗
fontWeight: '600',
// 英文字体保持较粗但避免过度发黑
fontWeight: '700',
},
sceneryCard: {
// 风景模式下稍微收窄文案宽度,增加呼吸感
@@ -810,7 +830,6 @@ const styles = StyleSheet.create({
},
actions: {
position: 'absolute',
bottom: SCREEN_HEIGHT * 0.16,
left: 0,
right: 0,
flexDirection: 'row',

View File

@@ -1,9 +1,13 @@
import Constants from 'expo-constants';
import { StyleSheet, Text, View } from 'react-native';
import { Platform, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
import { useTranslation } from 'react-i18next';
import { clampContentWidth, isIPadLike } from '@/src/utils/device';
export default function SettingsScreen() {
const { t } = useTranslation();
const { width, height } = useWindowDimensions();
const isTablet = isIPadLike(width, height);
const contentWidth = isTablet ? clampContentWidth(width, 720, 24) : undefined;
const version =
Constants.expoConfig?.version ??
@@ -12,6 +16,7 @@ export default function SettingsScreen() {
return (
<View style={styles.container}>
<View style={[styles.contentWrap, contentWidth ? { width: contentWidth } : null]}>
<View style={styles.section}>
<Text style={styles.label}>{t('settings.version')}</Text>
<Text style={styles.value}>{version}</Text>
@@ -21,12 +26,18 @@ export default function SettingsScreen() {
<Text style={styles.cardTitle}>{t('settings.widgetTitle')}</Text>
<Text style={styles.cardText}>{t('settings.widgetDesc')}</Text>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, gap: 16 },
container: { flex: 1, width: '100%', alignSelf: 'stretch', padding: 16 },
contentWrap: {
width: '100%',
alignSelf: 'center',
gap: 16,
},
section: {
borderRadius: 14,
padding: 16,
@@ -38,7 +49,12 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
},
label: { color: '#374151', fontSize: 16 },
value: { color: '#111827', fontSize: 16, fontWeight: '600' },
value: {
color: '#111827',
fontSize: 16,
fontWeight: '600',
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : undefined,
},
card: {
borderRadius: 16,
padding: 16,

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions, Platform, Alert, Image } from 'react-native';
import { View, Text, StyleSheet, TouchableOpacity, Platform, Alert, Image, useWindowDimensions } from 'react-native';
import { useRouter } from 'expo-router';
import * as WebBrowser from 'expo-web-browser';
import { Trans, useTranslation } from 'react-i18next';
@@ -14,8 +14,6 @@ import { isTraditionalChineseLocaleTag } from '@/src/i18n/locale';
import FlowersBg from '../../assets/images/index/flowers_endbg.svg';
import WelcomeBtn from '../../assets/images/index/welcome_btn.svg';
const { width, height } = Dimensions.get('window');
// 繁中開屏 consent 文案:寫死在元件內,避免 Metro/iOS bundle 快取導致永遠顯示舊文案。
// 若需修改,請改這裡並同步 client/src/i18n/locales/zh-TW.json 的 consent 區塊。
const ZH_TW_CONSENT = {
@@ -27,7 +25,9 @@ const ZH_TW_CONSENT = {
export default function SplashScreen() {
const router = useRouter();
const { t, i18n } = useTranslation();
const { width, height } = useWindowDimensions();
const [showConsent, setShowConsent] = useState(false);
const isTablet = Platform.OS === 'ios' && Math.min(width, height) >= 768;
// 繁中時強制使用上方常數(含 zh-TW / zh-Hant / zh-Hant-TW其餘用 i18n
const isZhTW = isTraditionalChineseLocaleTag(i18n.language || '');
@@ -127,52 +127,67 @@ export default function SplashScreen() {
void refreshLegalLinks();
}, []);
const bgDecorationTop = 363;
const bgDecorationHeight = height * 0.6;
const bgDecorationHeight = isTablet ? Math.round(height * 0.55) : height * 0.6;
const bgDecorationTop = height - bgDecorationHeight;
const contentTop = bgDecorationTop + (bgDecorationHeight * 0.25);
const contentWidth = isTablet ? Math.min(640, Math.floor(width * 0.76)) : width;
const topImageWidth = isTablet ? Math.min(430, Math.floor(width * 0.48)) : 308;
const topImageHeight = isTablet ? Math.min(460, Math.floor(height * 0.45)) : 354;
const topImageMarginTop = isTablet ? 148 : 60;
const bgWidth = width + 10;
const bgLeft = -3;
const buttonSize = isTablet ? { width: 108, height: 70 } : { width: 87, height: 57 };
const bottomOffset = isTablet ? 28 : 60;
const buttonBottomGap = isTablet ? 28 : 40;
const noticeStyle = isTablet
? { fontSize: 14, lineHeight: 20, paddingHorizontal: 32, maxWidth: contentWidth }
: null;
const noticeLinkStyle = isTablet ? { fontSize: 14 } : null;
const titleStyle = isTablet ? { fontSize: 50, lineHeight: 60 } : null;
const subtitleStyle = isTablet ? { marginTop: 10, fontSize: 18, lineHeight: 24 } : null;
return (
<View style={styles.container}>
{/* 中间的背景装饰 SVG (现在放在上面,作为上层) */}
<View style={[styles.bgDecorationContainer, { top: bgDecorationTop }]}>
<FlowersBg width={width + 10} height={bgDecorationHeight} />
<View style={[styles.bgDecorationContainer, { bottom: 0, left: bgLeft }]}>
<FlowersBg width={bgWidth} height={bgDecorationHeight} preserveAspectRatio="none" />
</View>
{/* 顶部的花图片 (现在放在下面,作为下层) */}
<View style={styles.topImageContainer}>
<View style={[styles.topImageContainer, { marginTop: topImageMarginTop }]}>
<Image
source={require('../../assets/images/index/index_flowers.png')}
style={styles.topImage}
style={[styles.topImage, { width: topImageWidth, height: topImageHeight }]}
resizeMode="contain"
/>
</View>
{/* 文案内容:主標題兩行 + 可選二級標題(字號更小、顏色更淺);繁中為元件內常數,其餘用 i18n */}
<View style={[styles.contentContainer, { position: 'absolute', top: contentTop }]}>
<Text style={styles.titleText}>
<View style={[styles.contentContainer, { position: 'absolute', top: contentTop, width: contentWidth }]}>
<Text style={[styles.titleText, titleStyle]}>
{title}
{'\n'}
{subtitle}
</Text>
{subtitleSecondary ? (
<Text style={styles.consentSubtitleSecondary}>{subtitleSecondary}</Text>
<Text style={[styles.consentSubtitleSecondary, subtitleStyle]}>{subtitleSecondary}</Text>
) : null}
</View>
<SafeAreaView style={styles.bottomContainer} edges={['bottom']}>
<SafeAreaView style={[styles.bottomContainer, { bottom: bottomOffset, width: contentWidth }]} edges={['bottom']}>
{showConsent && (
<>
<TouchableOpacity
onPress={handleAgree}
activeOpacity={0.8}
style={styles.buttonWrapper}
style={[styles.buttonWrapper, { marginBottom: buttonBottomGap }]}
accessibilityRole="button"
accessibilityLabel={t('consent.agree')}
>
<WelcomeBtn width={87} height={57} />
<WelcomeBtn width={buttonSize.width} height={buttonSize.height} />
</TouchableOpacity>
<Text style={styles.noticeText}>
<Text style={[styles.noticeText, noticeStyle]}>
<Trans
i18nKey="consent.noticeRich"
values={{
@@ -184,14 +199,14 @@ export default function SplashScreen() {
components={{
privacy: (
<Text
style={[styles.noticeLinkText, !links.privacy && styles.noticeLinkTextDisabled]}
style={[styles.noticeLinkText, noticeLinkStyle, !links.privacy && styles.noticeLinkTextDisabled]}
onPress={() => void handleOpenLegal('privacy')}
suppressHighlighting
/>
),
terms: (
<Text
style={[styles.noticeLinkText, !links.terms && styles.noticeLinkTextDisabled]}
style={[styles.noticeLinkText, noticeLinkStyle, !links.terms && styles.noticeLinkTextDisabled]}
onPress={() => void handleOpenLegal('terms')}
suppressHighlighting
/>
@@ -209,11 +224,13 @@ export default function SplashScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
width: '100%',
height: '100%',
alignSelf: 'stretch',
backgroundColor: '#F5D3B5', // 匹配 Figma 背景色
alignItems: 'center',
},
topImageContainer: {
marginTop: 60,
zIndex: 1, // 降低层级
},
topImage: {
@@ -222,7 +239,6 @@ const styles = StyleSheet.create({
},
bgDecorationContainer: {
position: 'absolute',
left: -3,
zIndex: 2, // 提高层级,使其覆盖在图片之上
},
contentContainer: {
@@ -247,8 +263,6 @@ const styles = StyleSheet.create({
},
bottomContainer: {
position: 'absolute',
bottom: 60,
width: '100%',
alignItems: 'center',
zIndex: 4,
},

View File

@@ -1,18 +1,25 @@
import { Link, Stack } from 'expo-router';
import { StyleSheet } from 'react-native';
import { StyleSheet, useWindowDimensions } from 'react-native';
import { Text, View } from '@/components/Themed';
import { clampContentWidth, isIPadLike } from '@/src/utils/device';
export default function NotFoundScreen() {
const { width, height } = useWindowDimensions();
const isTablet = isIPadLike(width, height);
const contentWidth = isTablet ? clampContentWidth(width, 680, 24) : undefined;
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<View style={styles.container}>
<Text style={styles.title}>This screen doesn't exist.</Text>
<View style={[styles.contentWrap, contentWidth ? { width: contentWidth } : null]}>
<Text style={styles.title}>This screen doesn't exist.</Text>
<Link href="/" style={styles.link}>
<Text style={styles.linkText}>Go to home screen!</Text>
</Link>
<Link href="/" style={styles.link}>
<Text style={styles.linkText}>Go to home screen!</Text>
</Link>
</View>
</View>
</>
);
@@ -24,6 +31,12 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
padding: 20,
width: '100%',
alignSelf: 'stretch',
},
contentWrap: {
width: '100%',
alignItems: 'center',
},
title: {
fontSize: 20,

View File

@@ -1,14 +1,18 @@
import { useEffect } from 'react';
import { ActivityIndicator, StyleSheet, View } from 'react-native';
import { ActivityIndicator, StyleSheet, View, useWindowDimensions } from 'react-native';
import { useRouter } from 'expo-router';
import { getOnboardingCompleted, getConsentAccepted } from '@/src/storage/appStorage';
import { clampContentWidth, isIPadLike } from '@/src/utils/device';
/**
* 启动分发:根据 consent 和 onboarding 状态跳转
*/
export default function Index() {
const router = useRouter();
const { width, height } = useWindowDimensions();
const isTablet = isIPadLike(width, height);
const loaderWidth = isTablet ? clampContentWidth(width, 680, 24) : undefined;
useEffect(() => {
let cancelled = false;
@@ -41,11 +45,24 @@ export default function Index() {
return (
<View style={styles.container}>
<ActivityIndicator />
<View style={[styles.loaderWrap, loaderWidth ? { width: loaderWidth } : null]}>
<ActivityIndicator />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
container: {
flex: 1,
width: '100%',
height: '100%',
alignSelf: 'stretch',
alignItems: 'center',
justifyContent: 'center',
},
loaderWrap: {
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@@ -1,15 +1,22 @@
import { StatusBar } from 'expo-status-bar';
import { Platform, StyleSheet } from 'react-native';
import { Platform, StyleSheet, useWindowDimensions } from 'react-native';
import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';
import { clampContentWidth, isIPadLike } from '@/src/utils/device';
export default function ModalScreen() {
const { width, height } = useWindowDimensions();
const isTablet = isIPadLike(width, height);
const contentWidth = isTablet ? clampContentWidth(width, 700, 24) : undefined;
return (
<View style={styles.container}>
<Text style={styles.title}>Modal</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/modal.tsx" />
<View style={[styles.contentWrap, contentWidth ? { width: contentWidth } : null]}>
<Text style={styles.title}>Modal</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/modal.tsx" />
</View>
{/* Use a light status bar on iOS to account for the black space above the modal */}
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
@@ -22,6 +29,12 @@ const styles = StyleSheet.create({
flex: 1,
alignItems: 'center',
justifyContent: 'center',
width: '100%',
alignSelf: 'stretch',
},
contentWrap: {
width: '100%',
alignItems: 'center',
},
title: {
fontSize: 20,

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState, useRef, useCallback } from 'react';
import { Alert, FlatList, Image, Pressable, StyleSheet, Text, View, Platform, Dimensions } from 'react-native';
import { Alert, FlatList, Image, Pressable, StyleSheet, Text, View, Platform, useWindowDimensions } from 'react-native';
import { useTranslation } from 'react-i18next';
import { LinearGradient } from 'expo-linear-gradient';
import { Switch } from 'react-native';
@@ -42,8 +42,7 @@ import * as Notifications from 'expo-notifications';
import { changeLanguage } from '@/src/i18n';
import { fetchLegalLinks } from '@/src/services/legalApi';
import { ensurePushTokenRegisteredIfPermitted, setPushPreferences } from '@/src/services/pushApi';
const { width } = Dimensions.get('window');
import { isIPadLike } from '@/src/utils/device';
type Props = {
visible: boolean;
@@ -79,6 +78,12 @@ const NATURE_IMAGES = [
export default function ProfileModal({ visible, name: propName, onClose }: Props) {
const { t } = useTranslation();
const { width, height } = useWindowDimensions();
const isTablet = isIPadLike(width, height);
const contentWidth = isTablet ? width - 40 : width - 40;
const thumbWidth = isTablet ? Math.min(420, contentWidth - 120) : Math.min(width * 0.6, 300);
const widgetImageWidth = isTablet ? Math.min(520, contentWidth - 24) : width * 0.9;
const howToSlideWidth = isTablet ? Math.min(640, contentWidth) : width - 32;
const [page, setPage] = useState<Page>('root');
const [navDirection, setNavDirection] = useState<NavDirection>('forward');
@@ -192,6 +197,7 @@ export default function ProfileModal({ visible, name: propName, onClose }: Props
{page === 'root' ? (
<RootPage
name={currentName}
contentWidth={contentWidth}
onOpenFavorites={() => go('favorites', 'forward')}
onOpenWidget={() => go('widget', 'forward')}
onOpenDailyReminder={() => go('dailyReminder', 'forward')}
@@ -200,15 +206,15 @@ export default function ProfileModal({ visible, name: propName, onClose }: Props
onOpenTerms={() => openLink(legalLinks.terms)}
/>
) : page === 'favorites' ? (
<FavoritesPage visible={visible} page={page} />
<FavoritesPage visible={visible} page={page} thumbWidth={thumbWidth} contentWidth={contentWidth} />
) : page === 'dailyReminder' ? (
<DailyReminderPage visible={visible} onDone={() => go('root', 'back')} />
<DailyReminderPage visible={visible} onDone={() => go('root', 'back')} contentWidth={contentWidth} />
) : page === 'language' ? (
<LanguagePage />
<LanguagePage contentWidth={contentWidth} />
) : page === 'widgetHowTo' ? (
<WidgetHowToPage />
<WidgetHowToPage howToSlideWidth={howToSlideWidth} widgetImageWidth={widgetImageWidth} />
) : (
<WidgetPage onOpenHowTo={() => go('widgetHowTo', 'forward')} />
<WidgetPage onOpenHowTo={() => go('widgetHowTo', 'forward')} widgetImageWidth={widgetImageWidth} />
)}
</Animated.View>
</View>
@@ -222,6 +228,7 @@ function toastTodo(t: (key: string) => string) {
function RootPage({
name,
contentWidth,
onOpenFavorites,
onOpenWidget,
onOpenDailyReminder,
@@ -230,6 +237,7 @@ function RootPage({
onOpenTerms,
}: {
name?: string;
contentWidth: number;
onOpenFavorites: () => void;
onOpenWidget: () => void;
onOpenDailyReminder: () => void;
@@ -239,7 +247,7 @@ function RootPage({
}) {
const { t } = useTranslation();
return (
<>
<View style={[styles.sectionWrap, { width: contentWidth }]}>
<View style={styles.header}>
<AvatarIcon width={234} height={183} />
<Text style={styles.name}>{name || 'Hali'}</Text>
@@ -276,11 +284,23 @@ function RootPage({
onPress={onOpenLanguage}
/>
</View>
</>
<Text style={styles.versionText}>V1.0.0</Text>
</View>
);
}
function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
function FavoritesPage({
visible,
page,
thumbWidth,
contentWidth,
}: {
visible: boolean;
page: Page;
thumbWidth: number;
contentWidth: number;
}) {
const { t } = useTranslation();
const [favorites, setFavorites] = useState<(FavoriteItem & { text: string })[]>([]);
@@ -317,7 +337,7 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
}
return (
<View style={styles.favContainer}>
<View style={[styles.favContainer, { width: contentWidth, alignSelf: 'center' }]}>
{favorites.length === 0 ? (
<Text style={styles.favEmpty}>{t('favorites.empty')}</Text>
) : (
@@ -339,6 +359,7 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
<View style={styles.favRight}>
<View style={[
styles.favThumb,
{ width: thumbWidth },
item.themeMode === 'scenery' ? {} : { backgroundColor: item.background }
]}>
{item.themeMode === 'scenery' ? (
@@ -346,7 +367,7 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
<Image
source={NATURE_IMAGES[parseInt(item.background)]}
style={{
width: width * 0.6,
width: thumbWidth,
height: 800, // 假设原图较高,设置一个较大的高度
position: 'absolute',
bottom: 0, // 关键:将图片底部对齐容器底部
@@ -378,7 +399,15 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
);
}
function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () => void }) {
function DailyReminderPage({
visible,
onDone,
contentWidth,
}: {
visible: boolean;
onDone: () => void;
contentWidth: number;
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [timesPerDay, setTimesPerDay] = useState(3);
@@ -525,7 +554,7 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
</Pressable>
</View>
<View style={styles.remindRow}>
<View style={[styles.remindRow, { width: contentWidth }]}>
<View style={styles.rowLeft}>
<View style={styles.rowIcon}>
<RemindIcon width={18} height={18} />
@@ -556,7 +585,7 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
);
}
function WidgetPage({ onOpenHowTo }: { onOpenHowTo: () => void }) {
function WidgetPage({ onOpenHowTo, widgetImageWidth }: { onOpenHowTo: () => void; widgetImageWidth: number }) {
const { t, i18n } = useTranslation();
const currentLang = i18n.language;
// 需求:个人主页弹窗「小工具」页暂时隐藏锁屏小工具说明/入口
@@ -580,13 +609,13 @@ function WidgetPage({ onOpenHowTo }: { onOpenHowTo: () => void }) {
<View style={styles.widgetScroll}>
{showLockScreenWidget ? (
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
<Image source={widget1} style={styles.widgetImg1} resizeMode="contain" />
<Image source={widget1} style={[styles.widgetImg1, { width: widgetImageWidth, height: widgetImageWidth * (156 / 311) }]} resizeMode="contain" />
<Text style={styles.widgetLabel}>{t('widget.lockScreen')}</Text>
</Pressable>
) : null}
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
<Image source={widget2} style={styles.widgetImg2} resizeMode="contain" />
<Image source={widget2} style={[styles.widgetImg2, { width: widgetImageWidth, height: widgetImageWidth * (175 / 311) }]} resizeMode="contain" />
<Text style={styles.widgetLabel}>{t('widget.homeScreen')}</Text>
</Pressable>
</View>
@@ -594,7 +623,7 @@ function WidgetPage({ onOpenHowTo }: { onOpenHowTo: () => void }) {
);
}
function WidgetHowToPage() {
function WidgetHowToPage({ howToSlideWidth, widgetImageWidth }: { howToSlideWidth: number; widgetImageWidth: number }) {
const { t, i18n } = useTranslation();
const currentLang = i18n.language;
const flatListRef = useRef<FlatList>(null);
@@ -631,7 +660,7 @@ function WidgetHowToPage() {
const onScroll = (event: any) => {
const x = event.nativeEvent.contentOffset.x;
const index = Math.round(x / (width - 32));
const index = Math.round(x / howToSlideWidth);
if (index !== activeIndex) {
setActiveIndex(index);
}
@@ -655,14 +684,14 @@ function WidgetHowToPage() {
onScrollBeginDrag={onScrollBeginDrag}
scrollEventThrottle={16}
renderItem={({ item }) => (
<View style={styles.howToSlide}>
<Image source={item.src} style={styles.howToImg} resizeMode="contain" />
<View style={[styles.howToSlide, { width: howToSlideWidth }]}>
<Image source={item.src} style={[styles.howToImg, { width: widgetImageWidth, height: widgetImageWidth * (234 / 326) }]} resizeMode="contain" />
<Text style={styles.howToDesc}>{item.desc}</Text>
</View>
)}
/>
<View style={styles.pagination}>
<View style={[styles.pagination, { top: widgetImageWidth * (234 / 326) + 35 }]}>
{images.map((_, i) => (
<View
key={i}
@@ -677,7 +706,7 @@ function WidgetHowToPage() {
);
}
function LanguagePage() {
function LanguagePage({ contentWidth }: { contentWidth: number }) {
const { t, i18n } = useTranslation();
const currentLang = i18n.language;
@@ -687,8 +716,8 @@ function LanguagePage() {
];
return (
<View style={styles.langPage}>
<View style={styles.langList}>
<View style={[styles.langPage, { width: contentWidth, alignSelf: 'center' }]}>
<View style={[styles.langList, { width: contentWidth }]}>
{languages.map((lang, index) => (
<Pressable
key={lang.id}
@@ -750,6 +779,9 @@ const styles = StyleSheet.create({
pageWrap: {
// 给页面切换动画一个稳定的容器,避免布局抖动
},
sectionWrap: {
alignSelf: 'center',
},
backRow: {
alignSelf: 'flex-start',
paddingVertical: 4,
@@ -806,6 +838,13 @@ const styles = StyleSheet.create({
overflow: 'hidden',
marginBottom: 8,
},
versionText: {
marginTop: 12,
textAlign: 'center',
color: 'rgba(94,42,40,0.45)',
fontSize: 12,
fontWeight: '500',
},
item: {
height: 52,
paddingHorizontal: 18,
@@ -848,7 +887,7 @@ const styles = StyleSheet.create({
paddingVertical: 100,
},
favList: {
paddingHorizontal: 20,
paddingHorizontal: 8,
paddingBottom: 80,
},
favCard: {
@@ -872,7 +911,6 @@ const styles = StyleSheet.create({
backgroundColor: '#FFF4EA',
borderRadius: 16,
padding: 20,
width: width * 0.6,
height: 161,
justifyContent: 'center',
position: 'relative',
@@ -945,7 +983,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 18,
width: width - 40, // 屏幕宽度减去左右各 20pt
alignSelf: 'center',
},
rowLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
@@ -1004,12 +1041,12 @@ const styles = StyleSheet.create({
width: '100%',
},
widgetImg1: {
width: width * 0.9,
height: (width * 0.9) * (156 / 311),
width: 320,
height: 160,
},
widgetImg2: {
width: width * 0.9,
height: (width * 0.9) * (175 / 311),
width: 320,
height: 176,
},
widgetLabel: {
marginTop: 12,
@@ -1023,12 +1060,12 @@ const styles = StyleSheet.create({
paddingTop: 20,
},
howToSlide: {
width: width - 32, // 减去 SheetModal 的 paddingHorizontal: 16 * 2
width: 320,
alignItems: 'center',
},
howToImg: {
width: width * 0.9,
height: (width * 0.9) * (234 / 326),
width: 320,
height: 230,
marginBottom: 40,
},
howToDesc: {
@@ -1042,7 +1079,7 @@ const styles = StyleSheet.create({
pagination: {
flexDirection: 'row',
position: 'absolute',
top: (width * 0.9) * (234 / 326) + 35, // 根据新的图片高度动态计算
top: 265,
gap: 8,
},
dot: {
@@ -1064,7 +1101,6 @@ const styles = StyleSheet.create({
backgroundColor: '#FFFFFF',
borderRadius: 20,
overflow: 'hidden',
width: width - 40, // 屏幕宽度减去左右各 20pt
alignSelf: 'center',
},
langItem: {

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
import { Image, Pressable, StyleSheet, Text, View, Platform, useWindowDimensions } from 'react-native';
import { useTranslation } from 'react-i18next';
import SheetModal from '@/components/ui/SheetModal';
@@ -15,8 +15,11 @@ type Props = {
export default function ThemeModal({ visible, mode, onSelect, onClose }: Props) {
const { t } = useTranslation();
const { width, height } = useWindowDimensions();
const isTablet = Platform.OS === 'ios' && Math.min(width, height) >= 768;
return (
<SheetModal visible={visible} title={t('theme.title')} onClose={onClose} height={360}>
<SheetModal visible={visible} title={t('theme.title')} onClose={onClose} height={isTablet ? 560 : 360}>
<View style={styles.row}>
<ThemeCard
title={t('theme.scenery')}
@@ -100,9 +103,9 @@ const styles = StyleSheet.create({
row: {
flexDirection: 'row',
flexWrap: 'nowrap',
gap: 12,
paddingHorizontal: 4,
paddingBottom: 50,
gap: 8,
paddingHorizontal: 0,
paddingBottom: 22,
paddingTop: 20,
justifyContent: 'space-between',
},

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { View, StyleSheet, TextInput, Platform, Animated, TouchableOpacity, Text, Keyboard, Pressable } from 'react-native';
import { View, StyleSheet, TextInput, Platform, Animated, TouchableOpacity, Text, Keyboard, Pressable, useWindowDimensions } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { OnboardingColors } from '@/constants/OnboardingTheme';
@@ -16,10 +16,13 @@ interface NameInputStepProps {
export function NameInputStep({ value, onChangeText, onNext }: NameInputStepProps) {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const { width, height } = useWindowDimensions();
const isTablet = Platform.OS === 'ios' && Math.min(width, height) >= 768;
const [isFocused, setIsFocused] = useState(false);
const [keyboardHeight, setKeyboardHeight] = useState(0);
const blinkAnim = useRef(new Animated.Value(1)).current;
const hasInput = value.trim().length > 0;
const inputCardWidth = isTablet ? Math.min(520, Math.floor(width * 0.72)) : 335;
useEffect(() => {
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
@@ -62,7 +65,7 @@ export function NameInputStep({ value, onChangeText, onNext }: NameInputStepProp
return (
<Pressable style={styles.container} onPress={Keyboard.dismiss} accessible={false}>
<View style={styles.inputCard}>
<View style={[styles.inputCard, { width: inputCardWidth }]}>
<View style={styles.inputWrapper}>
{/* 显示层:文案 + 跟随的光标 */}
<View style={styles.displayLayer}>

View File

@@ -1,5 +1,5 @@
import React, { useRef, useEffect } from 'react';
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform, Animated, Easing } from 'react-native';
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform, Animated, Easing, useWindowDimensions } from 'react-native';
import { useTranslation } from 'react-i18next';
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
@@ -29,6 +29,9 @@ export function OnboardingLayout({
userName = '',
}: OnboardingLayoutProps) {
const { t } = useTranslation();
const { width, height } = useWindowDimensions();
const isTablet = Platform.OS === 'ios' && Math.min(width, height) >= 768;
const contentMaxWidth = isTablet ? 620 : undefined;
const showGreeting = currentStep === 1 && userName.trim().length > 0;
const displayName = userName.trim();
const prevStepRef = useRef(currentStep);
@@ -72,7 +75,7 @@ export function OnboardingLayout({
<StatusBar barStyle="dark-content" />
<SafeAreaView style={styles.safeArea}>
{/* Header: Back & Skip */}
<View style={styles.header}>
<View style={[styles.header, contentMaxWidth ? { maxWidth: contentMaxWidth, width: '100%', alignSelf: 'center' } : null]}>
<View style={styles.headerLeft}>
{showBackButton && onBack && (
<TouchableOpacity onPress={onBack} style={styles.iconButton}>
@@ -94,7 +97,7 @@ export function OnboardingLayout({
</View>
{/* Title & Progress Row名字步骤后第一步且名字非空时显示招呼语 + 问题) */}
<View style={styles.titleRow}>
<View style={[styles.titleRow, contentMaxWidth ? { maxWidth: contentMaxWidth, width: '100%', alignSelf: 'center' } : null]}>
<View style={styles.titleBlock}>
{showGreeting && (
<Text style={styles.greetingText}>{t('onboardingSurvey.greeting', { name: displayName })}</Text>
@@ -108,6 +111,7 @@ export function OnboardingLayout({
<Animated.View
style={[
styles.content,
contentMaxWidth ? { maxWidth: contentMaxWidth, width: '100%', alignSelf: 'center' } : null,
{
opacity,
transform: [{ translateX }],

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, Text, Platform, ActivityIndicator } from 'react-native';
import { View, StyleSheet, TouchableOpacity, Text, Platform, ActivityIndicator, useWindowDimensions } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { LinearGradient } from 'expo-linear-gradient';
@@ -20,6 +20,9 @@ interface ReminderStepProps {
export function ReminderStep({ value, onChange, onFinish, loading = false }: ReminderStepProps) {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const { width, height } = useWindowDimensions();
const isTablet = Platform.OS === 'ios' && Math.min(width, height) >= 768;
const contentMaxWidth = isTablet ? 560 : undefined;
const handleReduce = () => {
// 本页最小为 1不接收提醒请使用右上角 Skip
@@ -32,7 +35,7 @@ export function ReminderStep({ value, onChange, onFinish, loading = false }: Rem
return (
<View style={styles.container}>
<View style={styles.counterContainer}>
<View style={[styles.counterContainer, contentMaxWidth ? { maxWidth: contentMaxWidth } : null]}>
<TouchableOpacity onPress={handleReduce} disabled={loading} activeOpacity={0.7}>
<ReduceIcon width={47} height={47} />
</TouchableOpacity>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, ScrollView, Text } from 'react-native';
import { View, StyleSheet, TouchableOpacity, ScrollView, Text, Platform, useWindowDimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg';
@@ -19,9 +19,12 @@ interface SelectionStepProps {
}
export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }: SelectionStepProps) {
const { width, height } = useWindowDimensions();
const isTablet = Platform.OS === 'ios' && Math.min(width, height) >= 768;
const maxOptionWidth = isTablet ? 560 : undefined;
const hasSelection = selectedIds.length > 0;
const insets = useSafeAreaInsets();
const footerBottom = insets.bottom + 16;
const footerBottom = insets.bottom + (isTablet ? 38 : 28);
const footerButtonHeight = 57;
// 底部留白加大,避免最后一项与按钮边框视觉重叠
const footerPaddingBottom = footerBottom + footerButtonHeight + 40;
@@ -31,14 +34,24 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }
<ScrollView
style={styles.scroll}
showsVerticalScrollIndicator={false}
contentContainerStyle={[styles.optionsList, { paddingBottom: footerPaddingBottom }]}
contentContainerStyle={[
styles.optionsList,
{
paddingBottom: footerPaddingBottom,
alignItems: 'center',
},
]}
>
{options.map((option) => {
const isSelected = selectedIds.includes(option.id);
return (
<TouchableOpacity
key={option.id}
style={[styles.optionCard, isSelected && styles.optionCardSelected]}
style={[
styles.optionCard,
maxOptionWidth ? { maxWidth: maxOptionWidth } : null,
isSelected && styles.optionCardSelected,
]}
onPress={() => onToggle(option.id)}
activeOpacity={0.7}
>

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState, useRef } from 'react';
import { Modal, Pressable, StyleSheet, Text, View, PanResponder, Animated as RNAnimated, Dimensions, Image, ImageSourcePropType } from 'react-native';
import { Modal, Pressable, StyleSheet, Text, View, PanResponder, Animated as RNAnimated, Image, ImageSourcePropType, Platform, useWindowDimensions } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, {
@@ -10,7 +10,6 @@ import Animated, {
withTiming,
} from 'react-native-reanimated';
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
const FIXED_TOP_GAP = 100; // 统一距离顶部的高度
type Props = {
@@ -30,11 +29,13 @@ type Props = {
export default function SheetModal({ visible, title, onClose, children, leftIcon, height: customHeight }: Props) {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const isTablet = Platform.OS === 'ios' && Math.min(windowWidth, windowHeight) >= 768;
const [mounted, setMounted] = useState(false);
const progress = useSharedValue(0); // 0: 关闭, 1: 打开
const dragY = useSharedValue(0); // 拖拽位移
const sheetHeight = customHeight || (SCREEN_HEIGHT - FIXED_TOP_GAP);
const sheetHeight = customHeight || (isTablet ? Math.min(windowHeight - 72, 760) : (windowHeight - FIXED_TOP_GAP));
useEffect(() => {
if (visible) {
@@ -90,7 +91,10 @@ export default function SheetModal({ visible, title, onClose, children, leftIcon
};
});
const containerPaddingBottom = useMemo(() => Math.max(insets.bottom, 80), [insets.bottom]); // 增加底部间距至 80约占 350 高度的 22%,确保内容不被截断并留出足够呼吸感
const containerPaddingBottom = useMemo(() => {
if (customHeight) return Math.max(insets.bottom, 16);
return Math.max(insets.bottom, isTablet ? 20 : 80);
}, [customHeight, insets.bottom, isTablet]);
// 注意Modal 的 visible 必须为 true 才会渲染,因此用 mounted 保持退场动画
return (
@@ -107,6 +111,7 @@ export default function SheetModal({ visible, title, onClose, children, leftIcon
{...panResponder.panHandlers}
style={[
styles.sheet,
isTablet ? styles.sheetTablet : null,
sheetStyle,
{
height: sheetHeight,
@@ -159,6 +164,14 @@ const styles = StyleSheet.create({
paddingTop: 8,
paddingHorizontal: 16,
},
sheetTablet: {
width: '100%',
alignSelf: 'stretch',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
handleContainer: {
alignItems: 'center',
paddingVertical: 8,

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -11,7 +11,7 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E3328F0E595C1F4A244DF238 /* libPods-client.a */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */; };
A1B2C3D4E5F60718293A4B5C /* 情绪小组件/EmotionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */; };
A8C1D2E3F4A5B6C7D8E9F0A2 /* AppGroupStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */; };
A8C1D2E3F4A5B6C7D8E9F0A3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
A8C1D2E3F4A5B6C7D8E9F0B2 /* AppGroupStorageBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */; };
@@ -54,7 +54,7 @@
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = client/Info.plist; sourceTree = "<group>"; };
3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-client.release.xcconfig"; path = "Target Support Files/Pods-client/Pods-client.release.xcconfig"; sourceTree = "<group>"; };
75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = client/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; sourceTree = "<group>"; };
A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; sourceTree = "<group>"; };
A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppGroupStorage.swift; path = client/AppGroupStorage.swift; sourceTree = "<group>"; };
A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = AppGroupStorageBridge.m; path = client/AppGroupStorageBridge.m; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = client/SplashScreen.storyboard; sourceTree = "<group>"; };
@@ -74,7 +74,7 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */ = {
EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
EmotionWidget.swift,
@@ -85,18 +85,7 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = "情绪小组件";
sourceTree = "<group>";
};
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = "情绪小组件"; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -213,7 +202,7 @@
EB3DAFD42F2A5FC100450593 /* Recovered References */ = {
isa = PBXGroup;
children = (
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */,
A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */,
);
name = "Recovered References";
sourceTree = "<group>";
@@ -482,7 +471,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */,
A1B2C3D4E5F60718293A4B5C /* 情绪小组件/EmotionWidget.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -536,7 +525,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "client/client-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -576,7 +565,7 @@
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "client/client-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
@@ -756,7 +745,7 @@
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@@ -808,7 +797,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};

View File

@@ -38,8 +38,6 @@
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocalNetworkUsageDescription</key>
<string>用于连接局域网服务以获取内容与同步数据(仅在需要访问内网地址时使用)。</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
@@ -47,6 +45,8 @@
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>用于连接局域网服务以获取内容与同步数据(仅在需要访问内网地址时使用)。</string>
<key>NSUserActivityTypes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
@@ -61,6 +61,8 @@
</array>
<key>UIRequiresFullScreen</key>
<false/>
<key>UIStatusBarHidden</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
@@ -72,8 +74,6 @@
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Automatic</string>

View File

@@ -280,12 +280,12 @@ struct EmotionWidgetView: View {
Text(entry.text)
.font(fontForFamily())
.foregroundColor(widgetTextColor)
.multilineTextAlignment(.leading)
.multilineTextAlignment(.center)
.lineSpacing(lineSpacingForFamily())
.lineLimit(lineLimitForFamily())
.minimumScaleFactor(0.78)
.padding(paddingForFamily())
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.widgetSolidBackground(widgetBackgroundColor)
.widgetURL(deepLink)
}

View File

@@ -0,0 +1,9 @@
import { Platform } from 'react-native';
export function isIPadLike(width: number, height: number): boolean {
return Platform.OS === 'ios' && Math.min(width, height) >= 768;
}
export function clampContentWidth(width: number, maxWidth: number, horizontalPadding: number): number {
return Math.min(maxWidth, Math.max(0, width - horizontalPadding * 2));
}