lucy-cli 2.0.0-beta.4 → 2.0.0-beta.6
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/dist/commands/checks.d.ts +2 -2
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/git.d.ts +2 -2
- package/dist/commands/home.d.ts +2 -2
- package/dist/commands/home.js +25 -2
- package/dist/commands/home.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/init/blocks.d.ts +1 -1
- package/dist/init/cargo.d.ts +1 -1
- package/dist/init/expo.d.ts +1 -1
- package/dist/init/gitModules.d.ts +1 -1
- package/dist/init/index.d.ts +1 -2
- package/dist/init/monorepo.d.ts +1 -1
- package/dist/init/prepareVelo.d.ts +1 -1
- package/dist/init/tauri.d.ts +1 -1
- package/dist/init/templates.d.ts +1 -1
- package/dist/init/velo.d.ts +1 -1
- package/dist/tasks/index.d.ts +1 -1
- package/dist/wix-sdk/index.d.ts +1 -1
- package/dist/wix-sdk/init.d.ts +1 -1
- package/dist/wix-sdk/run.d.ts +1 -1
- package/dist/wix-sync/index.d.ts +1 -1
- package/dist/wix-sync/init.d.ts +1 -1
- package/files/templates/expo verse[D]/files/.nvmrc +1 -0
- package/files/templates/expo verse[D]/files/.prettierignore +23 -0
- package/files/templates/expo verse[D]/files/.prettierrc.js +16 -0
- package/files/templates/expo verse[D]/files/.stylelintrc.json +8 -0
- package/files/templates/expo verse[D]/files/.vscode/extensions.json +10 -0
- package/files/templates/expo verse[D]/files/.vscode/launch.json +54 -0
- package/files/templates/expo verse[D]/files/.vscode/settings.json +30 -0
- package/files/templates/expo verse[D]/files/.vscode/tasks.json +0 -0
- package/files/templates/expo verse[D]/files/.yarnrc +1 -0
- package/files/templates/expo verse[D]/files/.yarnrc.yml +8 -0
- package/files/templates/expo verse[D]/files/App.tsx +63 -0
- package/files/templates/expo verse[D]/files/DEBUGGING.md +126 -0
- package/files/templates/expo verse[D]/files/README.md +45 -0
- package/files/templates/expo verse[D]/files/android/app/build.gradle +177 -0
- package/files/templates/expo verse[D]/files/android/app/debug.keystore +0 -0
- package/files/templates/expo verse[D]/files/android/app/proguard-rules.pro +14 -0
- package/files/templates/expo verse[D]/files/android/app/src/debug/AndroidManifest.xml +7 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/AndroidManifest.xml +33 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/java/so/sunnysideup/daily_verse/MainActivity.kt +65 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/java/so/sunnysideup/daily_verse/MainApplication.kt +57 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-night-hdpi/splashscreen_logo.png +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-night-mdpi/splashscreen_logo.png +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-night-xhdpi/splashscreen_logo.png +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-night-xxhdpi/splashscreen_logo.png +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-night-xxxhdpi/splashscreen_logo.png +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/values/colors.xml +6 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/values/strings.xml +7 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/values/styles.xml +12 -0
- package/files/templates/expo verse[D]/files/android/app/src/main/res/values-night/colors.xml +3 -0
- package/files/templates/expo verse[D]/files/android/build.gradle +37 -0
- package/files/templates/expo verse[D]/files/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/files/templates/expo verse[D]/files/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/files/templates/expo verse[D]/files/android/gradle.properties +59 -0
- package/files/templates/expo verse[D]/files/android/gradlew +251 -0
- package/files/templates/expo verse[D]/files/android/gradlew.bat +94 -0
- package/files/templates/expo verse[D]/files/android/settings.gradle +39 -0
- package/files/templates/expo verse[D]/files/app.config.ts +77 -0
- package/files/templates/expo verse[D]/files/assets/data/de.csv +949 -0
- package/files/templates/expo verse[D]/files/assets/data/en.csv +949 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-Black.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-BlackItalic.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-Bold.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-BoldItalic.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-ExtraBold.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-ExtraBoldItalic.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-Italic.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-Medium.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-MediumItalic.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-Regular.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-SemiBold.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-SemiBoldItalic.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/fonts/SpaceMono-Regular.ttf +0 -0
- package/files/templates/expo verse[D]/files/assets/images/andorid/background.png +0 -0
- package/files/templates/expo verse[D]/files/assets/images/android-dark.png +0 -0
- package/files/templates/expo verse[D]/files/assets/images/android-light.png +0 -0
- package/files/templates/expo verse[D]/files/assets/images/background.png +0 -0
- package/files/templates/expo verse[D]/files/assets/images/ios-dark.png +0 -0
- package/files/templates/expo verse[D]/files/assets/images/ios-light.png +0 -0
- package/files/templates/expo verse[D]/files/assets/images/ios-tinted.png +0 -0
- package/files/templates/expo verse[D]/files/assets/images/splash-icon-dark.png +0 -0
- package/files/templates/expo verse[D]/files/assets/images/splash-icon-light.png +0 -0
- package/files/templates/expo verse[D]/files/babel.config.js +10 -0
- package/files/templates/expo verse[D]/files/components/.gitkeep +0 -0
- package/files/templates/expo verse[D]/files/components/MainScreen.tsx +299 -0
- package/files/templates/expo verse[D]/files/components/Share.tsx +132 -0
- package/files/templates/expo verse[D]/files/components/ui/.gitkeep +0 -0
- package/files/templates/expo verse[D]/files/components/ui/dev.tsx +48 -0
- package/files/templates/expo verse[D]/files/constants/config.ts +30 -0
- package/files/templates/expo verse[D]/files/constants/theme.ts +18 -0
- package/files/templates/expo verse[D]/files/data/de.csv +219 -0
- package/files/templates/expo verse[D]/files/data/en.csv +219 -0
- package/files/templates/expo verse[D]/files/data/version.json +4 -0
- package/files/templates/expo verse[D]/files/eas.json +47 -0
- package/files/templates/expo verse[D]/files/eslint.config.mjs +185 -0
- package/files/templates/expo verse[D]/files/global.css +47 -0
- package/files/templates/expo verse[D]/files/hooks/useColorScheme.ts +17 -0
- package/files/templates/expo verse[D]/files/index.ts +28 -0
- package/files/templates/expo verse[D]/files/lib/content.ts +31 -0
- package/files/templates/expo verse[D]/files/lib/data.ts +180 -0
- package/files/templates/expo verse[D]/files/lib/helper.ts +54 -0
- package/files/templates/expo verse[D]/files/lib/index.ts +21 -0
- package/files/templates/expo verse[D]/files/lib/state.ts +128 -0
- package/files/templates/expo verse[D]/files/lib/storage.ts +17 -0
- package/files/templates/expo verse[D]/files/lib/taskManager/index.ts +38 -0
- package/files/templates/expo verse[D]/files/lib/taskManager/loadData.ts +14 -0
- package/files/templates/expo verse[D]/files/lib/utils/index.ts +11 -0
- package/files/templates/expo verse[D]/files/lib/utils/polyfills.ts +29 -0
- package/files/templates/expo verse[D]/files/lib/utils/screenshot.ts +77 -0
- package/files/templates/expo verse[D]/files/lucy.json +8 -0
- package/files/templates/expo verse[D]/files/metro.config.js +81 -0
- package/files/templates/expo verse[D]/files/models/index.ts +28 -0
- package/files/templates/expo verse[D]/files/nativewind-env.d.ts +1 -0
- package/files/templates/expo verse[D]/files/package.json +102 -0
- package/files/templates/expo verse[D]/files/pnpm-workspace.yaml +3 -0
- package/files/templates/expo verse[D]/files/scripts/convert-verses.ts +309 -0
- package/files/templates/expo verse[D]/files/scripts/move-artifacts.ts +83 -0
- package/files/templates/expo verse[D]/files/scripts/reset-project.ts +116 -0
- package/files/templates/expo verse[D]/files/tailwind.config.js +63 -0
- package/files/templates/expo verse[D]/files/tsconfig.json +45 -0
- package/files/templates/expo verse[D]/files/types/index.ts +4 -0
- package/files/templates/expo verse[D]/files/types/reset.d.ts +1 -0
- package/files/templates/expo verse[D]/lucy.json +86 -0
- package/files/templates/expo[D]/files/app.config.ts +69 -0
- package/files/templates/expo[D]/files/assets/fonts/SpaceMono-Regular.ttf +0 -0
- package/files/templates/expo[D]/files/scripts/reset-project.ts +0 -0
- package/old/index.ts +0 -0
- package/package.json +11 -12
- package/pnpm-workspace.yaml +6 -0
- package/src/commands/home.ts +36 -2
- package/src/index.ts +0 -0
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
File without changes
|
@@ -0,0 +1,299 @@
|
|
1
|
+
import { Effect } from 'effect';
|
2
|
+
import * as Haptics from 'expo-haptics';
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
4
|
+
import { Animated, AppState, AppStateStatus, ImageBackground, PanResponder, StyleSheet, Text, View } from 'react-native';
|
5
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
6
|
+
import colors from 'tailwindcss/colors';
|
7
|
+
|
8
|
+
import { DATA_REFRESH_DELAY, MIN_PULL_DISTANCE_THRESHOLD, PULL_BACK_DURATION, PULL_DISTANCE_THRESHOLD } from '@/constants/config';
|
9
|
+
import { getContent } from '@/lib/content';
|
10
|
+
import { shareCurrentScreen } from '@/lib/utils/screenshot';
|
11
|
+
|
12
|
+
import Share from './Share';
|
13
|
+
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
15
|
+
export type EffectReturnSuccessType<T extends (...args: any[]) => Effect.Effect<any, any, any>> = Effect.Effect.Success<ReturnType<T>>;
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Main screen of the app.
|
19
|
+
* @returns The main screen of the app.
|
20
|
+
*/
|
21
|
+
export default function MainScreen() {
|
22
|
+
const insets = useSafeAreaInsets();
|
23
|
+
const [quote, setQuote] = useState<EffectReturnSuccessType<typeof getContent> | null>(null);
|
24
|
+
const [isLoading, setIsLoading] = useState(true);
|
25
|
+
const [error, setError] = useState<string | null>(null);
|
26
|
+
const appState = useRef(AppState.currentState);
|
27
|
+
const fadeAnim = useRef(new Animated.Value(0)).current;
|
28
|
+
const [isCapturing, setIsCapturing] = useState(false);
|
29
|
+
const hasTriggeredThresholdRef = useRef(false);
|
30
|
+
const pullDirectionRef = useRef<'up' | 'down' | null>(null);
|
31
|
+
const quoteRef = useRef<EffectReturnSuccessType<typeof getContent> | null>(null);
|
32
|
+
const fadeValueRef = useRef(0);
|
33
|
+
useEffect(() => {
|
34
|
+
quoteRef.current = quote;
|
35
|
+
}, [quote]);
|
36
|
+
useEffect(() => {
|
37
|
+
const id = fadeAnim.addListener(({ value }) => {
|
38
|
+
fadeValueRef.current = typeof value === 'number' ? value : Number(value);
|
39
|
+
});
|
40
|
+
|
41
|
+
return () => {
|
42
|
+
fadeAnim.removeListener(id);
|
43
|
+
};
|
44
|
+
}, [fadeAnim]);
|
45
|
+
|
46
|
+
// Pull feedback state
|
47
|
+
const [pullDirection, setPullDirection] = useState<'up' | 'down' | null>(null);
|
48
|
+
const pullFeedbackOpacity = useRef(new Animated.Value(0)).current;
|
49
|
+
const animatedPullDistance = useRef(new Animated.Value(0)).current;
|
50
|
+
const panResponder = useRef(
|
51
|
+
PanResponder.create({
|
52
|
+
onStartShouldSetPanResponder: () => true,
|
53
|
+
onMoveShouldSetPanResponder: (event, gestureState) => Math.abs(gestureState.dy) > MIN_PULL_DISTANCE_THRESHOLD,
|
54
|
+
onPanResponderMove: (event, gestureState) => {
|
55
|
+
const { dy } = gestureState;
|
56
|
+
if(dy > 0) return;
|
57
|
+
if (Math.abs(dy) > MIN_PULL_DISTANCE_THRESHOLD) {
|
58
|
+
const direction = dy > 0 ? 'down' : 'up';
|
59
|
+
setPullDirection(direction);
|
60
|
+
pullDirectionRef.current = direction;
|
61
|
+
animatedPullDistance.setValue(Math.abs(dy));
|
62
|
+
// Animate feedback opacity based on pull distance with more subtle range
|
63
|
+
const opacity = Math.min(Math.abs(dy) / 80, 1); // Reduced from 100 to 80 for more responsive feel
|
64
|
+
Animated.timing(pullFeedbackOpacity, {
|
65
|
+
toValue: opacity,
|
66
|
+
duration: 0,
|
67
|
+
useNativeDriver: true,
|
68
|
+
}).start();
|
69
|
+
}
|
70
|
+
// Trigger once when crossing the threshold during this gesture
|
71
|
+
if (!hasTriggeredThresholdRef.current && Math.abs(dy) >= PULL_DISTANCE_THRESHOLD) {
|
72
|
+
hasTriggeredThresholdRef.current = true;
|
73
|
+
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Soft);
|
74
|
+
}
|
75
|
+
},
|
76
|
+
onPanResponderRelease: (event, gestureState) => {
|
77
|
+
// Hide pull feedback
|
78
|
+
const releasedDirection: 'up' | 'down' = gestureState.dy > 0 ? 'down' : 'up';
|
79
|
+
const finalDirection = pullDirectionRef.current ?? releasedDirection;
|
80
|
+
setPullDirection(null);
|
81
|
+
if(finalDirection === 'down') return;
|
82
|
+
|
83
|
+
// Run slide-back and fade-out in parallel
|
84
|
+
Animated.parallel([
|
85
|
+
Animated.timing(animatedPullDistance, {
|
86
|
+
toValue: 0,
|
87
|
+
duration: PULL_BACK_DURATION,
|
88
|
+
useNativeDriver: true,
|
89
|
+
}),
|
90
|
+
Animated.timing(pullFeedbackOpacity, {
|
91
|
+
toValue: 0,
|
92
|
+
duration: PULL_BACK_DURATION,
|
93
|
+
useNativeDriver: true,
|
94
|
+
}),
|
95
|
+
]).start();
|
96
|
+
hasTriggeredThresholdRef.current = false;
|
97
|
+
pullDirectionRef.current = null;
|
98
|
+
|
99
|
+
if ((finalDirection === 'up' || finalDirection === 'down') && (gestureState.dy <= -PULL_DISTANCE_THRESHOLD || gestureState.dy >= PULL_DISTANCE_THRESHOLD)) {
|
100
|
+
void Haptics.notificationAsync(
|
101
|
+
Haptics.NotificationFeedbackType.Success
|
102
|
+
);
|
103
|
+
void captureAndShare();
|
104
|
+
}
|
105
|
+
},
|
106
|
+
})
|
107
|
+
).current;
|
108
|
+
|
109
|
+
const captureAndShare = async () => {
|
110
|
+
if (!quoteRef.current) return;
|
111
|
+
// Hide overlay visuals immediately and cancel any running overlay animations
|
112
|
+
pullFeedbackOpacity.stopAnimation();
|
113
|
+
animatedPullDistance.stopAnimation();
|
114
|
+
pullFeedbackOpacity.setValue(0);
|
115
|
+
animatedPullDistance.setValue(0);
|
116
|
+
setIsCapturing(true);
|
117
|
+
try {
|
118
|
+
// Ensure quote is fully visible during capture
|
119
|
+
fadeAnim.stopAnimation();
|
120
|
+
fadeAnim.setValue(1);
|
121
|
+
// Wait a frame so UI updates are applied
|
122
|
+
await new Promise<void>((resolve) => requestAnimationFrame(() => setTimeout(() => resolve(), 16)));
|
123
|
+
await shareCurrentScreen(quoteRef.current?.Reference.trim() ?? '');
|
124
|
+
} finally {
|
125
|
+
// Keep quote fully visible; keep overlay suppressed for a moment to avoid flicker
|
126
|
+
fadeAnim.setValue(1);
|
127
|
+
pullFeedbackOpacity.setValue(0);
|
128
|
+
animatedPullDistance.setValue(0);
|
129
|
+
setTimeout(() => setIsCapturing(false), 200);
|
130
|
+
}
|
131
|
+
};
|
132
|
+
|
133
|
+
useEffect(() => {
|
134
|
+
const getQuoteForToday = async (isInitialLoad: boolean) => {
|
135
|
+
if (isInitialLoad) {
|
136
|
+
setIsLoading(true);
|
137
|
+
}
|
138
|
+
setError(null);
|
139
|
+
|
140
|
+
try {
|
141
|
+
const newQuote = await Effect.runPromise(getContent());
|
142
|
+
setQuote(newQuote);
|
143
|
+
// Start fade-in animation after quote is loaded
|
144
|
+
if (isInitialLoad) {
|
145
|
+
setTimeout(() => {
|
146
|
+
Animated.timing(fadeAnim, {
|
147
|
+
toValue: 1,
|
148
|
+
duration: 1500,
|
149
|
+
useNativeDriver: true,
|
150
|
+
}).start();
|
151
|
+
}, 500); // 1500ms delay before animation starts
|
152
|
+
}
|
153
|
+
} catch (e) {
|
154
|
+
// eslint-disable-next-line no-console
|
155
|
+
console.error('Failed to fetch or store quote:', e);
|
156
|
+
setError('Could not fetch a quote. Please try again later.');
|
157
|
+
} finally {
|
158
|
+
if (isInitialLoad) {
|
159
|
+
setIsLoading(false);
|
160
|
+
}
|
161
|
+
}
|
162
|
+
};
|
163
|
+
void getQuoteForToday(true);
|
164
|
+
const intervalId = setInterval(() => {
|
165
|
+
void getQuoteForToday(false);
|
166
|
+
}, DATA_REFRESH_DELAY);
|
167
|
+
|
168
|
+
return () => clearInterval(intervalId);
|
169
|
+
}, [fadeAnim]);
|
170
|
+
|
171
|
+
useEffect(() => {
|
172
|
+
// Subscribe to app state changes
|
173
|
+
const appStateSubscription = AppState.addEventListener(
|
174
|
+
'change',
|
175
|
+
(nextAppState: AppStateStatus) => {
|
176
|
+
if (
|
177
|
+
appState.current.match(/inactive|background/) &&
|
178
|
+
nextAppState === 'active'
|
179
|
+
) {
|
180
|
+
setTimeout(() => {
|
181
|
+
Animated.timing(fadeAnim, {
|
182
|
+
toValue: 1,
|
183
|
+
duration: 1500,
|
184
|
+
useNativeDriver: true,
|
185
|
+
}).start();
|
186
|
+
}, 500); //
|
187
|
+
|
188
|
+
}
|
189
|
+
if (appState.current.match(/active/) && nextAppState === 'background') {
|
190
|
+
Animated.timing(fadeAnim, {
|
191
|
+
toValue: 0,
|
192
|
+
duration: 0,
|
193
|
+
useNativeDriver: true,
|
194
|
+
}).start();
|
195
|
+
}
|
196
|
+
appState.current = nextAppState;
|
197
|
+
}
|
198
|
+
);
|
199
|
+
|
200
|
+
// Cleanup subscription on unmount
|
201
|
+
return () => {
|
202
|
+
appStateSubscription.remove();
|
203
|
+
};
|
204
|
+
}, [fadeAnim]);
|
205
|
+
|
206
|
+
return (
|
207
|
+
<ImageBackground
|
208
|
+
source={require('@/assets/images/background.png')}
|
209
|
+
style={styles.container}
|
210
|
+
resizeMode="cover"
|
211
|
+
|
212
|
+
>
|
213
|
+
<View style={[styles.titleWrapper, { top: insets.top + 10 }]}>
|
214
|
+
<Text style={styles.titleText}>Daily Verse</Text>
|
215
|
+
</View>
|
216
|
+
|
217
|
+
<View style={styles.contentWrapper}>
|
218
|
+
{/* {isLoading && (
|
219
|
+
<Text style={styles.titleText}>Loading...</Text>
|
220
|
+
)}
|
221
|
+
{error && <Text style={styles.errorText}>{error}</Text>} */}
|
222
|
+
{!isLoading && !error && quote && (
|
223
|
+
<Animated.View style={[styles.quoteBox, { opacity: fadeAnim }]}>
|
224
|
+
<Text style={styles.quoteContent}>"{quote.Verse}"</Text>
|
225
|
+
<Text style={styles.quoteSource}>— {quote.Reference}</Text>
|
226
|
+
</Animated.View>
|
227
|
+
)}
|
228
|
+
</View>
|
229
|
+
|
230
|
+
{/* Pull feedback overlay */}
|
231
|
+
<Share
|
232
|
+
pullDirection={pullDirection}
|
233
|
+
opacity={pullFeedbackOpacity}
|
234
|
+
animatedPullDistance={animatedPullDistance}
|
235
|
+
hidden={isCapturing}
|
236
|
+
/>
|
237
|
+
|
238
|
+
{/* Gesture overlay to capture double taps and vertical pulls */}
|
239
|
+
<View style={styles.gestureOverlay} {...panResponder.panHandlers} />
|
240
|
+
|
241
|
+
</ImageBackground>
|
242
|
+
);
|
243
|
+
}
|
244
|
+
|
245
|
+
const styles = StyleSheet.create({
|
246
|
+
container: {
|
247
|
+
flex: 1,
|
248
|
+
backgroundColor: '#000',
|
249
|
+
height: '100%',
|
250
|
+
width: '100%',
|
251
|
+
},
|
252
|
+
gestureOverlay: {
|
253
|
+
position: 'absolute',
|
254
|
+
top: 0,
|
255
|
+
bottom: 0,
|
256
|
+
left: 0,
|
257
|
+
right: 0,
|
258
|
+
zIndex: 999,
|
259
|
+
},
|
260
|
+
titleWrapper: {
|
261
|
+
position: 'absolute',
|
262
|
+
left: 0,
|
263
|
+
right: 0,
|
264
|
+
alignItems: 'center',
|
265
|
+
},
|
266
|
+
titleText: {
|
267
|
+
color: colors.gray[900],
|
268
|
+
fontFamily: 'Playfair-Bold',
|
269
|
+
fontSize: 36,
|
270
|
+
},
|
271
|
+
contentWrapper: {
|
272
|
+
flex: 1,
|
273
|
+
justifyContent: 'center',
|
274
|
+
alignItems: 'center',
|
275
|
+
paddingHorizontal: 24,
|
276
|
+
},
|
277
|
+
quoteBox: {
|
278
|
+
gap: 20,
|
279
|
+
alignSelf: 'stretch',
|
280
|
+
},
|
281
|
+
quoteContent: {
|
282
|
+
fontSize: 24,
|
283
|
+
textAlign: 'center',
|
284
|
+
color: colors.gray[900],
|
285
|
+
lineHeight: 32,
|
286
|
+
fontFamily: 'Playfair-Regular',
|
287
|
+
},
|
288
|
+
quoteSource: {
|
289
|
+
fontSize: 16,
|
290
|
+
color: colors.gray[800],
|
291
|
+
textAlign: 'right',
|
292
|
+
fontFamily: 'Playfair-Bold',
|
293
|
+
},
|
294
|
+
errorText: {
|
295
|
+
textAlign: 'center',
|
296
|
+
color: colors.red[500],
|
297
|
+
fontSize: 18,
|
298
|
+
}
|
299
|
+
});
|
@@ -0,0 +1,132 @@
|
|
1
|
+
import AntDesign from '@expo/vector-icons/AntDesign';
|
2
|
+
import React, { useEffect, useRef } from 'react';
|
3
|
+
import { Animated, AppState, AppStateStatus, Dimensions, StyleSheet, View } from 'react-native';
|
4
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
5
|
+
|
6
|
+
import { MAX_OVERLAY_HEIGHT } from '@/constants/config';
|
7
|
+
|
8
|
+
const { width: screenWidth } = Dimensions.get('window');
|
9
|
+
|
10
|
+
/**
|
11
|
+
* Props for the PullFeedback component
|
12
|
+
*/
|
13
|
+
interface PullFeedbackProps {
|
14
|
+
pullDirection: 'up' | 'down' | null;
|
15
|
+
opacity: Animated.Value;
|
16
|
+
animatedPullDistance: Animated.Value;
|
17
|
+
hidden?: boolean;
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* PullFeedback component that shows a stretching semi-circle based on pull gestures
|
22
|
+
* @param props - The component props
|
23
|
+
* @param props.pullDirection - Direction of the pull ('up' or 'down')
|
24
|
+
* @param props.opacity - Animated opacity value
|
25
|
+
* @param props.animatedPullDistance - Optional animated distance value from parent for native-driven perf
|
26
|
+
* @param props.hidden - Whether to hide the component
|
27
|
+
* @returns The PullFeedback component
|
28
|
+
*/
|
29
|
+
export default function Share({ pullDirection, opacity, animatedPullDistance, hidden = false }: PullFeedbackProps) {
|
30
|
+
const insets = useSafeAreaInsets();
|
31
|
+
|
32
|
+
// Ensure hooks run consistently; default direction when inactive
|
33
|
+
const activeDirection = pullDirection ?? 'up';
|
34
|
+
|
35
|
+
// Fixed container height (revealed via sliding inner content)
|
36
|
+
const containerHeight = MAX_OVERLAY_HEIGHT + (activeDirection === 'up' ? insets.bottom : insets.top);
|
37
|
+
|
38
|
+
// Map distance -> visible height with rubber-band feel (approximation)
|
39
|
+
const visibleHeight = animatedPullDistance.interpolate({
|
40
|
+
inputRange: [0, 100, MAX_OVERLAY_HEIGHT],
|
41
|
+
outputRange: [0, MAX_OVERLAY_HEIGHT * 0.7, MAX_OVERLAY_HEIGHT],
|
42
|
+
extrapolate: 'clamp',
|
43
|
+
});
|
44
|
+
|
45
|
+
// base = containerHeight - visibleHeight
|
46
|
+
// up: translateY = base, down: translateY = -base
|
47
|
+
const base = Animated.subtract(containerHeight, visibleHeight);
|
48
|
+
const translateY = activeDirection === 'up' ? base : (Animated.multiply(base, -1) as unknown as Animated.AnimatedInterpolation<number>);
|
49
|
+
|
50
|
+
const topOffset = activeDirection === 'down' ? 0 : undefined;
|
51
|
+
const bottomOffset = activeDirection === 'up' ? 0 : undefined;
|
52
|
+
const bottomSafeArea = activeDirection === 'up' ? insets.bottom : 0;
|
53
|
+
const appState = useRef(AppState.currentState);
|
54
|
+
|
55
|
+
useEffect(() => {
|
56
|
+
// Subscribe to app state changes
|
57
|
+
const appStateSubscription = AppState.addEventListener(
|
58
|
+
'change',
|
59
|
+
(nextAppState: AppStateStatus) => {
|
60
|
+
if (appState.current.match(/active/) && nextAppState === 'inactive') {
|
61
|
+
animatedPullDistance.stopAnimation();
|
62
|
+
animatedPullDistance.setValue(0);
|
63
|
+
opacity.stopAnimation();
|
64
|
+
opacity.setValue(0);
|
65
|
+
}
|
66
|
+
appState.current = nextAppState;
|
67
|
+
}
|
68
|
+
);
|
69
|
+
|
70
|
+
// Cleanup subscription on unmount
|
71
|
+
return () => {
|
72
|
+
appStateSubscription.remove();
|
73
|
+
};
|
74
|
+
});
|
75
|
+
|
76
|
+
return (
|
77
|
+
<Animated.View
|
78
|
+
style={[
|
79
|
+
styles.container,
|
80
|
+
{
|
81
|
+
opacity: opacity.interpolate({
|
82
|
+
inputRange: [0, 1],
|
83
|
+
outputRange: [0, 0.6], // Reduced max opacity from 1 to 0.6
|
84
|
+
}),
|
85
|
+
top: topOffset,
|
86
|
+
bottom: bottomOffset,
|
87
|
+
height: containerHeight,
|
88
|
+
overflow: 'hidden',
|
89
|
+
}
|
90
|
+
,
|
91
|
+
// Force hide without interrupting animations
|
92
|
+
hidden && { opacity: 0 }
|
93
|
+
]}
|
94
|
+
>
|
95
|
+
<Animated.View
|
96
|
+
style={{
|
97
|
+
transform: [{ translateY }],
|
98
|
+
}}
|
99
|
+
>
|
100
|
+
<View style={[
|
101
|
+
styles.semiCircle,
|
102
|
+
{
|
103
|
+
width: screenWidth,
|
104
|
+
height: containerHeight,
|
105
|
+
borderTopLeftRadius: activeDirection === 'down' ? 0 : 35,
|
106
|
+
borderTopRightRadius: activeDirection === 'down' ? 0 : 35,
|
107
|
+
borderBottomLeftRadius: activeDirection === 'up' ? 0 : 35,
|
108
|
+
borderBottomRightRadius: activeDirection === 'up' ? 0 : 35,
|
109
|
+
}
|
110
|
+
]}>
|
111
|
+
<AntDesign name="sharealt" size={48} color="white" style={{ paddingBottom: bottomSafeArea }}/>
|
112
|
+
</View>
|
113
|
+
</Animated.View>
|
114
|
+
</Animated.View>
|
115
|
+
);
|
116
|
+
}
|
117
|
+
|
118
|
+
const styles = StyleSheet.create({
|
119
|
+
container: {
|
120
|
+
position: 'absolute',
|
121
|
+
left: 0,
|
122
|
+
right: 0,
|
123
|
+
zIndex: 998, // Below gesture overlay but above content
|
124
|
+
},
|
125
|
+
semiCircle: {
|
126
|
+
backgroundColor: 'rgba(255, 255, 255, 0.3)',
|
127
|
+
borderWidth: 2,
|
128
|
+
borderColor: 'rgba(255, 255, 255, 0.5)',
|
129
|
+
alignItems: 'center',
|
130
|
+
justifyContent: 'center',
|
131
|
+
},
|
132
|
+
});
|
File without changes
|
@@ -0,0 +1,48 @@
|
|
1
|
+
/* eslint-disable no-console */
|
2
|
+
import { Effect } from 'effect';
|
3
|
+
import * as BackgroundTask from 'expo-background-task';
|
4
|
+
import { Button, View } from 'react-native';
|
5
|
+
|
6
|
+
import { clearData, showData } from '@/lib';
|
7
|
+
import { BACKGROUND_TASK_IDENTIFIER, ENABLE_DEV_TOOLS } from '@/constants/config';
|
8
|
+
|
9
|
+
/**
|
10
|
+
* Development tools for the app.
|
11
|
+
* @returns The development tools for the app.
|
12
|
+
*/
|
13
|
+
export default function Dev() {
|
14
|
+
const isDev = ENABLE_DEV_TOOLS && __DEV__;
|
15
|
+
|
16
|
+
return (
|
17
|
+
<View>
|
18
|
+
{isDev && (
|
19
|
+
<>
|
20
|
+
<Button
|
21
|
+
title="Run Background Task (Debug)"
|
22
|
+
onPress={() => {
|
23
|
+
BackgroundTask.triggerTaskWorkerForTestingAsync().catch(console.error);
|
24
|
+
}}
|
25
|
+
/>
|
26
|
+
<Button
|
27
|
+
title="Clear Storage (Debug)"
|
28
|
+
onPress={() => {
|
29
|
+
Effect.runPromise(clearData).catch(console.error);
|
30
|
+
}}
|
31
|
+
/>
|
32
|
+
<Button
|
33
|
+
title="Show Data (Debug)"
|
34
|
+
onPress={() => {
|
35
|
+
Effect.runPromise(showData).catch(console.error);
|
36
|
+
}}
|
37
|
+
/>
|
38
|
+
<Button
|
39
|
+
title="Clear Tasks (Debug)"
|
40
|
+
onPress={() => {
|
41
|
+
BackgroundTask.unregisterTaskAsync(BACKGROUND_TASK_IDENTIFIER).catch(console.error);
|
42
|
+
}}
|
43
|
+
/>
|
44
|
+
</>
|
45
|
+
)}
|
46
|
+
</View>
|
47
|
+
);
|
48
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import { Schedule } from 'effect';
|
2
|
+
import { RuntimeFiber } from 'effect/Fiber';
|
3
|
+
import * as Application from 'expo-application';
|
4
|
+
import { Dimensions, Platform } from 'react-native';
|
5
|
+
|
6
|
+
import { Quote } from '@/models';
|
7
|
+
|
8
|
+
const { height: screenHeight } = Dimensions.get('window');
|
9
|
+
|
10
|
+
export const BASE_URL = new URL('https://static.we-host.ch/daily-verse');
|
11
|
+
export const FALLBACK_QUOTE: Quote = { Reference: 'The Lord', Verse: 'The ways of the Lord are inscrutable.', Topic: 'Grace' };
|
12
|
+
export const FALLBACK_LOCALE = 'en';
|
13
|
+
export const STORAGE_KEY = 'quotes';
|
14
|
+
export const STATE_KEY = 'state';
|
15
|
+
export const HTTP_RETRY_POLICY = Schedule.fromDelays(50, 100, 200, 400, 800);
|
16
|
+
export const backgroundTasks: RuntimeFiber<void, never>[] = [];
|
17
|
+
export const BACKGROUND_TASK_IDENTIFIER = 'fetch-data-task';
|
18
|
+
export const MINIMUM_INTERVAL = 60 * 24;
|
19
|
+
export const DATA_REFRESH_DELAY = 60000;
|
20
|
+
|
21
|
+
export const ENABLE_DEV_TOOLS = true;
|
22
|
+
|
23
|
+
export const MAX_OVERLAY_HEIGHT = (screenHeight * 0.25); // 30% of screen height
|
24
|
+
export const DOUBLE_TAP_DELAY_MS = 300;
|
25
|
+
export const MIN_PULL_DISTANCE_THRESHOLD = 10;
|
26
|
+
export const PULL_DISTANCE_THRESHOLD = MAX_OVERLAY_HEIGHT - 50;
|
27
|
+
export const PULL_BACK_DURATION = 350;
|
28
|
+
|
29
|
+
export const CACHE_CONTROL = 'max-age=43200';
|
30
|
+
export const USER_AGENT = `EXPO_APPLICATION/${Application.applicationName}:${Application.nativeApplicationVersion} (${Platform.OS} BUILD/${Application.nativeBuildVersion})`;
|
@@ -0,0 +1,18 @@
|
|
1
|
+
export const NAV_THEME = {
|
2
|
+
light: {
|
3
|
+
background: 'hsl(0 0% 100%)', // background
|
4
|
+
border: 'hsl(240 5.9% 90%)', // border
|
5
|
+
card: 'hsl(0 0% 100%)', // card
|
6
|
+
notification: 'hsl(0 84.2% 60.2%)', // destructive
|
7
|
+
primary: 'hsl(240 5.9% 10%)', // primary
|
8
|
+
text: 'hsl(240 10% 3.9%)', // foreground
|
9
|
+
},
|
10
|
+
dark: {
|
11
|
+
background: 'hsl(240 10% 3.9%)', // background
|
12
|
+
border: 'hsl(240 3.7% 15.9%)', // border
|
13
|
+
card: 'hsl(240 10% 3.9%)', // card
|
14
|
+
notification: 'hsl(0 72% 51%)', // destructive
|
15
|
+
primary: 'hsl(0 0% 98%)', // primary
|
16
|
+
text: 'hsl(0 0% 98%)', // foreground
|
17
|
+
},
|
18
|
+
};
|