lucy-cli 2.0.0-beta.3 → 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.
Files changed (204) hide show
  1. package/.wix/debug.log +10 -0
  2. package/dist/args.js +0 -1
  3. package/dist/args.js.map +1 -1
  4. package/dist/commands/checks.d.ts +2 -2
  5. package/dist/commands/edit.d.ts +1 -1
  6. package/dist/commands/exec.d.ts +1 -1
  7. package/dist/commands/exec.js +15 -13
  8. package/dist/commands/exec.js.map +1 -1
  9. package/dist/commands/git.d.ts +2 -2
  10. package/dist/commands/home.d.ts +2 -2
  11. package/dist/commands/home.js +25 -2
  12. package/dist/commands/home.js.map +1 -1
  13. package/dist/commands/install.d.ts +1 -0
  14. package/dist/commands/install.js +29 -16
  15. package/dist/commands/install.js.map +1 -1
  16. package/dist/commands/read.d.ts +3 -3
  17. package/dist/config.d.ts +1 -1
  18. package/dist/index.js +0 -0
  19. package/dist/init/blocks.d.ts +1 -1
  20. package/dist/init/blocks.js +2 -2
  21. package/dist/init/cargo.d.ts +1 -1
  22. package/dist/init/cargo.js +2 -2
  23. package/dist/init/expo.d.ts +1 -1
  24. package/dist/init/expo.js +2 -2
  25. package/dist/init/gitModules.d.ts +1 -1
  26. package/dist/init/index.d.ts +1 -2
  27. package/dist/init/index.js +7 -1
  28. package/dist/init/index.js.map +1 -1
  29. package/dist/init/monorepo.d.ts +1 -1
  30. package/dist/init/monorepo.js +2 -2
  31. package/dist/init/prepareVelo.d.ts +1 -1
  32. package/dist/init/tauri.d.ts +1 -1
  33. package/dist/init/tauri.js +2 -2
  34. package/dist/init/templates.d.ts +1 -1
  35. package/dist/init/velo.d.ts +1 -1
  36. package/dist/init/velo.js +3 -2
  37. package/dist/init/velo.js.map +1 -1
  38. package/dist/runtime.d.ts +1 -1
  39. package/dist/schemas/index.d.ts +1 -0
  40. package/dist/schemas/index.js +1 -0
  41. package/dist/schemas/index.js.map +1 -1
  42. package/dist/schemas/lucy.d.ts +2 -2
  43. package/dist/schemas/lucy.js +1 -1
  44. package/dist/schemas/lucy.js.map +1 -1
  45. package/dist/tasks/index.d.ts +1 -1
  46. package/dist/wix-sdk/index.d.ts +1 -1
  47. package/dist/wix-sdk/init.d.ts +1 -1
  48. package/dist/wix-sdk/run.d.ts +1 -1
  49. package/dist/wix-sync/index.d.ts +1 -1
  50. package/dist/wix-sync/init.d.ts +1 -1
  51. package/files/templates/expo verse[D]/files/.nvmrc +1 -0
  52. package/files/templates/expo verse[D]/files/.prettierignore +23 -0
  53. package/files/templates/expo verse[D]/files/.prettierrc.js +16 -0
  54. package/files/templates/expo verse[D]/files/.stylelintrc.json +8 -0
  55. package/files/templates/expo verse[D]/files/.vscode/extensions.json +10 -0
  56. package/files/templates/expo verse[D]/files/.vscode/launch.json +54 -0
  57. package/files/templates/expo verse[D]/files/.vscode/settings.json +30 -0
  58. package/files/templates/expo verse[D]/files/.vscode/tasks.json +0 -0
  59. package/files/templates/expo verse[D]/files/.yarnrc +1 -0
  60. package/files/templates/expo verse[D]/files/.yarnrc.yml +8 -0
  61. package/files/templates/expo verse[D]/files/App.tsx +63 -0
  62. package/files/templates/expo verse[D]/files/DEBUGGING.md +126 -0
  63. package/files/templates/expo verse[D]/files/README.md +45 -0
  64. package/files/templates/expo verse[D]/files/android/app/build.gradle +177 -0
  65. package/files/templates/expo verse[D]/files/android/app/debug.keystore +0 -0
  66. package/files/templates/expo verse[D]/files/android/app/proguard-rules.pro +14 -0
  67. package/files/templates/expo verse[D]/files/android/app/src/debug/AndroidManifest.xml +7 -0
  68. package/files/templates/expo verse[D]/files/android/app/src/main/AndroidManifest.xml +33 -0
  69. package/files/templates/expo verse[D]/files/android/app/src/main/java/so/sunnysideup/daily_verse/MainActivity.kt +65 -0
  70. package/files/templates/expo verse[D]/files/android/app/src/main/java/so/sunnysideup/daily_verse/MainApplication.kt +57 -0
  71. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
  72. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  73. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png +0 -0
  74. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png +0 -0
  75. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-night-hdpi/splashscreen_logo.png +0 -0
  76. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-night-mdpi/splashscreen_logo.png +0 -0
  77. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-night-xhdpi/splashscreen_logo.png +0 -0
  78. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-night-xxhdpi/splashscreen_logo.png +0 -0
  79. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-night-xxxhdpi/splashscreen_logo.png +0 -0
  80. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png +0 -0
  81. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png +0 -0
  82. package/files/templates/expo verse[D]/files/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png +0 -0
  83. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
  84. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  85. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  86. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp +0 -0
  87. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp +0 -0
  88. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  89. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  90. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp +0 -0
  91. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp +0 -0
  92. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  93. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  94. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp +0 -0
  95. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp +0 -0
  96. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  97. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  98. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp +0 -0
  99. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp +0 -0
  100. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  101. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  102. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp +0 -0
  103. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp +0 -0
  104. package/files/templates/expo verse[D]/files/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  105. package/files/templates/expo verse[D]/files/android/app/src/main/res/values/colors.xml +6 -0
  106. package/files/templates/expo verse[D]/files/android/app/src/main/res/values/strings.xml +7 -0
  107. package/files/templates/expo verse[D]/files/android/app/src/main/res/values/styles.xml +12 -0
  108. package/files/templates/expo verse[D]/files/android/app/src/main/res/values-night/colors.xml +3 -0
  109. package/files/templates/expo verse[D]/files/android/build.gradle +37 -0
  110. package/files/templates/expo verse[D]/files/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  111. package/files/templates/expo verse[D]/files/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  112. package/files/templates/expo verse[D]/files/android/gradle.properties +59 -0
  113. package/files/templates/expo verse[D]/files/android/gradlew +251 -0
  114. package/files/templates/expo verse[D]/files/android/gradlew.bat +94 -0
  115. package/files/templates/expo verse[D]/files/android/settings.gradle +39 -0
  116. package/files/templates/expo verse[D]/files/app.config.ts +77 -0
  117. package/files/templates/expo verse[D]/files/assets/data/de.csv +949 -0
  118. package/files/templates/expo verse[D]/files/assets/data/en.csv +949 -0
  119. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-Black.ttf +0 -0
  120. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-BlackItalic.ttf +0 -0
  121. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-Bold.ttf +0 -0
  122. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-BoldItalic.ttf +0 -0
  123. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-ExtraBold.ttf +0 -0
  124. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-ExtraBoldItalic.ttf +0 -0
  125. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-Italic.ttf +0 -0
  126. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-Medium.ttf +0 -0
  127. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-MediumItalic.ttf +0 -0
  128. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-Regular.ttf +0 -0
  129. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-SemiBold.ttf +0 -0
  130. package/files/templates/expo verse[D]/files/assets/fonts/PlayfairDisplay-SemiBoldItalic.ttf +0 -0
  131. package/files/templates/expo verse[D]/files/assets/fonts/SpaceMono-Regular.ttf +0 -0
  132. package/files/templates/expo verse[D]/files/assets/images/andorid/background.png +0 -0
  133. package/files/templates/expo verse[D]/files/assets/images/android-dark.png +0 -0
  134. package/files/templates/expo verse[D]/files/assets/images/android-light.png +0 -0
  135. package/files/templates/expo verse[D]/files/assets/images/background.png +0 -0
  136. package/files/templates/expo verse[D]/files/assets/images/ios-dark.png +0 -0
  137. package/files/templates/expo verse[D]/files/assets/images/ios-light.png +0 -0
  138. package/files/templates/expo verse[D]/files/assets/images/ios-tinted.png +0 -0
  139. package/files/templates/expo verse[D]/files/assets/images/splash-icon-dark.png +0 -0
  140. package/files/templates/expo verse[D]/files/assets/images/splash-icon-light.png +0 -0
  141. package/files/templates/expo verse[D]/files/babel.config.js +10 -0
  142. package/files/templates/expo verse[D]/files/components/.gitkeep +0 -0
  143. package/files/templates/expo verse[D]/files/components/MainScreen.tsx +299 -0
  144. package/files/templates/expo verse[D]/files/components/Share.tsx +132 -0
  145. package/files/templates/expo verse[D]/files/components/ui/.gitkeep +0 -0
  146. package/files/templates/expo verse[D]/files/components/ui/dev.tsx +48 -0
  147. package/files/templates/expo verse[D]/files/constants/config.ts +30 -0
  148. package/files/templates/expo verse[D]/files/constants/theme.ts +18 -0
  149. package/files/templates/expo verse[D]/files/data/de.csv +219 -0
  150. package/files/templates/expo verse[D]/files/data/en.csv +219 -0
  151. package/files/templates/expo verse[D]/files/data/version.json +4 -0
  152. package/files/templates/expo verse[D]/files/eas.json +47 -0
  153. package/files/templates/expo verse[D]/files/eslint.config.mjs +185 -0
  154. package/files/templates/expo verse[D]/files/global.css +47 -0
  155. package/files/templates/expo verse[D]/files/hooks/useColorScheme.ts +17 -0
  156. package/files/templates/expo verse[D]/files/index.ts +28 -0
  157. package/files/templates/expo verse[D]/files/lib/content.ts +31 -0
  158. package/files/templates/expo verse[D]/files/lib/data.ts +180 -0
  159. package/files/templates/expo verse[D]/files/lib/helper.ts +54 -0
  160. package/files/templates/expo verse[D]/files/lib/index.ts +21 -0
  161. package/files/templates/expo verse[D]/files/lib/state.ts +128 -0
  162. package/files/templates/expo verse[D]/files/lib/storage.ts +17 -0
  163. package/files/templates/expo verse[D]/files/lib/taskManager/index.ts +38 -0
  164. package/files/templates/expo verse[D]/files/lib/taskManager/loadData.ts +14 -0
  165. package/files/templates/expo verse[D]/files/lib/utils/index.ts +11 -0
  166. package/files/templates/expo verse[D]/files/lib/utils/polyfills.ts +29 -0
  167. package/files/templates/expo verse[D]/files/lib/utils/screenshot.ts +77 -0
  168. package/files/templates/expo verse[D]/files/lucy.json +8 -0
  169. package/files/templates/expo verse[D]/files/metro.config.js +81 -0
  170. package/files/templates/expo verse[D]/files/models/index.ts +28 -0
  171. package/files/templates/expo verse[D]/files/nativewind-env.d.ts +1 -0
  172. package/files/templates/expo verse[D]/files/package.json +102 -0
  173. package/files/templates/expo verse[D]/files/pnpm-workspace.yaml +3 -0
  174. package/files/templates/expo verse[D]/files/scripts/convert-verses.ts +309 -0
  175. package/files/templates/expo verse[D]/files/scripts/move-artifacts.ts +83 -0
  176. package/files/templates/expo verse[D]/files/scripts/reset-project.ts +116 -0
  177. package/files/templates/expo verse[D]/files/tailwind.config.js +63 -0
  178. package/files/templates/expo verse[D]/files/tsconfig.json +45 -0
  179. package/files/templates/expo verse[D]/files/types/index.ts +4 -0
  180. package/files/templates/expo verse[D]/files/types/reset.d.ts +1 -0
  181. package/files/templates/expo verse[D]/lucy.json +86 -0
  182. package/files/templates/expo[D]/files/app.config.ts +69 -0
  183. package/files/templates/expo[D]/files/assets/fonts/SpaceMono-Regular.ttf +0 -0
  184. package/files/templates/expo[D]/files/scripts/reset-project.ts +0 -0
  185. package/files/templates/velo[D]/files/.yarnrc.yml +1 -2
  186. package/files/templates/velo[D]/lucy.json +1 -1
  187. package/mitarbeiter-login.html +188 -0
  188. package/old/index.ts +0 -0
  189. package/package.json +11 -11
  190. package/pnpm-workspace.yaml +6 -0
  191. package/src/args.ts +0 -1
  192. package/src/commands/exec.ts +22 -18
  193. package/src/commands/home.ts +36 -2
  194. package/src/commands/install.ts +41 -21
  195. package/src/index.ts +0 -0
  196. package/src/init/blocks.ts +2 -2
  197. package/src/init/cargo.ts +2 -2
  198. package/src/init/expo.ts +2 -2
  199. package/src/init/index.ts +7 -1
  200. package/src/init/monorepo.ts +2 -2
  201. package/src/init/tauri.ts +2 -2
  202. package/src/init/velo.ts +4 -3
  203. package/src/schemas/index.ts +2 -0
  204. package/src/schemas/lucy.ts +1 -1
@@ -0,0 +1,10 @@
1
+ module.exports = function(api) {
2
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
3
+ api.cache(true);
4
+
5
+ return {
6
+ presets: [['babel-preset-expo', {
7
+ jsxImportSource: 'nativewind'
8
+ }], 'nativewind/babel'],
9
+ };
10
+ };
@@ -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}>&quot;{quote.Verse}&quot;</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
+ });
@@ -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
+ };