Compare commits
18 Commits
v0.1.0
...
c915d5f2ad
| Author | SHA1 | Date | |
|---|---|---|---|
| c915d5f2ad | |||
| fee283b5d6 | |||
| 67c132d88b | |||
| f1a4b58234 | |||
| dca76ea394 | |||
| af8ec98bb4 | |||
| c43b913dde | |||
| 256b338ae3 | |||
| ec95406305 | |||
| 6bd1dc8358 | |||
| d3d73c255e | |||
| 72cfbf41f3 | |||
| 86303e5c84 | |||
| c414ddc4fa | |||
| f0fda1947d | |||
| 8a75cffbd1 | |||
| 0b0226368b | |||
| 8f42971187 |
@@ -1,9 +1,9 @@
|
|||||||
name: Build Android APK
|
name: Release APK
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*.*.*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -12,7 +12,9 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
if: github.server_url == 'https://github.com'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
@@ -24,10 +26,14 @@ jobs:
|
|||||||
sudo rm -rf /usr/share/swift /usr/local/julia*
|
sudo rm -rf /usr/share/swift /usr/local/julia*
|
||||||
sudo apt-get autoremove -y > /dev/null 2>&1
|
sudo apt-get autoremove -y > /dev/null 2>&1
|
||||||
sudo apt-get clean
|
sudo apt-get clean
|
||||||
df -h
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
@@ -36,10 +42,15 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- uses: actions/setup-java@v4
|
- name: Set up Android SDK
|
||||||
with:
|
uses: android-actions/setup-android@v3
|
||||||
java-version: '17'
|
|
||||||
distribution: 'temurin'
|
- name: Accept Android SDK licenses
|
||||||
|
run: yes | sdkmanager --licenses || true
|
||||||
|
|
||||||
|
- name: Install SDK components
|
||||||
|
run: |
|
||||||
|
sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -47,31 +58,128 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
~/.gradle/wrapper
|
~/.gradle/wrapper
|
||||||
key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
key: ${{ runner.os }}-gradle-${{ hashFiles('package.json') }}
|
||||||
restore-keys: gradle-
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
- name: Generate Android native project
|
- name: Decode keystore
|
||||||
run: npx expo prebuild --platform android --clean
|
env:
|
||||||
|
KEYSTORE_B64: ${{ secrets.KEYSTORE_B64 }}
|
||||||
- name: Build debug APK
|
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
|
||||||
working-directory: android
|
KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
|
||||||
|
KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
chmod +x gradlew
|
mkdir -p ~/.config/sheethappens
|
||||||
./gradlew assembleDebug --no-daemon
|
echo "$KEYSTORE_B64" | base64 -d > ~/.config/sheethappens/sheethappens.jks
|
||||||
|
{
|
||||||
|
printf 'KEYSTORE_PATH=%s\n' "$HOME/.config/sheethappens/sheethappens.jks"
|
||||||
|
printf 'KEYSTORE_ALIAS=%q\n' "$KEYSTORE_ALIAS"
|
||||||
|
printf 'KEYSTORE_STORE_PASSWORD=%q\n' "$KEYSTORE_STORE_PASSWORD"
|
||||||
|
printf 'KEYSTORE_KEY_PASSWORD=%q\n' "$KEYSTORE_KEY_PASSWORD"
|
||||||
|
} > ~/.config/sheethappens/signing.env
|
||||||
|
chmod 600 ~/.config/sheethappens/signing.env ~/.config/sheethappens/sheethappens.jks
|
||||||
|
|
||||||
- name: Upload APK artifact
|
- name: Verify signing.env
|
||||||
uses: actions/upload-artifact@v4
|
run: |
|
||||||
with:
|
echo "Keys present in signing.env:"
|
||||||
name: SheetHappens-debug-${{ github.ref_name }}
|
grep -o '^[^=]*' ~/.config/sheethappens/signing.env
|
||||||
path: android/app/build/outputs/apk/debug/app-debug.apk
|
source ~/.config/sheethappens/signing.env
|
||||||
retention-days: 30
|
[ -n "$KEYSTORE_PATH" ] && echo "KEYSTORE_PATH: set" || echo "KEYSTORE_PATH: EMPTY"
|
||||||
|
[ -n "$KEYSTORE_ALIAS" ] && echo "KEYSTORE_ALIAS: set" || echo "KEYSTORE_ALIAS: EMPTY"
|
||||||
|
[ -n "$KEYSTORE_STORE_PASSWORD" ] && echo "KEYSTORE_STORE_PASSWORD: set" || echo "KEYSTORE_STORE_PASSWORD: EMPTY"
|
||||||
|
[ -n "$KEYSTORE_KEY_PASSWORD" ] && echo "KEYSTORE_KEY_PASSWORD: set" || echo "KEYSTORE_KEY_PASSWORD: EMPTY"
|
||||||
|
|
||||||
|
- name: Patch native modules for Android 15 16KB page size
|
||||||
|
run: |
|
||||||
|
WORKLETS="node_modules/react-native-worklets/android/CMakeLists.txt"
|
||||||
|
REANIMATED="node_modules/react-native-reanimated/android/CMakeLists.txt"
|
||||||
|
if [ -f "$WORKLETS" ] && ! grep -q "max-page-size" "$WORKLETS"; then
|
||||||
|
printf '\ntarget_link_options(worklets PRIVATE "-Wl,-z,max-page-size=16384")\n' >> "$WORKLETS"
|
||||||
|
fi
|
||||||
|
if [ -f "$REANIMATED" ] && ! grep -q "max-page-size" "$REANIMATED"; then
|
||||||
|
printf '\ntarget_link_options(reanimated PRIVATE "-Wl,-z,max-page-size=16384")\n' >> "$REANIMATED"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build signed APK
|
||||||
|
run: ./build-apk.sh
|
||||||
|
|
||||||
|
- name: Find built APK
|
||||||
|
id: apk
|
||||||
|
run: |
|
||||||
|
APK=$(ls dist/*.apk | sort | tail -1)
|
||||||
|
echo "path=$APK" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Generate changelog
|
||||||
|
id: changelog
|
||||||
|
run: |
|
||||||
|
PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${{ github.ref_name }}$" | head -1)
|
||||||
|
echo "Previous tag: $PREV_TAG"
|
||||||
|
echo "prev_tag=$PREV_TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
FEATS=$(git log "${PREV_TAG}..HEAD" --pretty=format:"%s" --no-merges \
|
||||||
|
| grep -E "^feat(\([^)]+\))?: " \
|
||||||
|
| sed -E 's/^feat(\([^)]+\))?: //' \
|
||||||
|
| sed 's/^/- /')
|
||||||
|
|
||||||
|
FIXES=$(git log "${PREV_TAG}..HEAD" --pretty=format:"%s" --no-merges \
|
||||||
|
| grep -E "^fix(\([^)]+\))?: " \
|
||||||
|
| sed -E 's/^fix(\([^)]+\))?: //' \
|
||||||
|
| sed 's/^/- /')
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "changelog<<CEOF"
|
||||||
|
[ -n "$FEATS" ] && printf "### What's New\n%s\n\n" "$FEATS"
|
||||||
|
[ -n "$FIXES" ] && printf "### Bug Fixes\n%s\n\n" "$FIXES"
|
||||||
|
echo "CEOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
name: "SheetHappens ${{ github.ref_name }}"
|
||||||
|
body: |
|
||||||
|
## SheetHappens ${{ github.ref_name }}
|
||||||
|
|
||||||
|
${{ steps.changelog.outputs.changelog }}
|
||||||
|
[Full changelog](https://github.com/${{ github.repository }}/compare/${{ steps.changelog.outputs.prev_tag }}...${{ github.ref_name }}) · [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
1. Enable "Unknown sources" on the device
|
||||||
|
2. Transfer the APK to the device and open it to install
|
||||||
|
files: ${{ steps.apk.outputs.path }}
|
||||||
|
draft: false
|
||||||
|
prerelease: ${{ contains(github.ref_name, '-') }}
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
PREV_TAG: ${{ steps.changelog.outputs.prev_tag }}
|
||||||
|
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
run: |
|
run: |
|
||||||
gh release create ${{ github.ref_name }} \
|
if [ -z "$GITEA_TOKEN" ]; then
|
||||||
android/app/build/outputs/apk/debug/app-debug.apk#SheetHappens-${{ github.ref_name }}.apk \
|
echo "GITEA_TOKEN not set — skipping Gitea release"
|
||||||
--title "SheetHappens ${{ github.ref_name }}" \
|
exit 0
|
||||||
--generate-notes
|
fi
|
||||||
|
|
||||||
|
BODY="## SheetHappens ${TAG}
|
||||||
|
|
||||||
|
${CHANGELOG}
|
||||||
|
|
||||||
|
[Full changelog](https://homegit.gyozamancave.fr/billisdead/SheetHappens/compare/${PREV_TAG}...${TAG}) · [CHANGELOG.md](https://homegit.gyozamancave.fr/billisdead/SheetHappens/raw/branch/main/CHANGELOG.md)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
Download the APK from the [GitHub release](https://github.com/${GH_REPO}/releases/tag/${TAG}), enable \"Unknown sources\" on the device and install."
|
||||||
|
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"https://homegit.gyozamancave.fr/api/v1/repos/billisdead/SheetHappens/releases" \
|
||||||
|
-d "{
|
||||||
|
\"tag_name\": \"${TAG}\",
|
||||||
|
\"name\": \"SheetHappens ${TAG}\",
|
||||||
|
\"body\": $(printf '%s' "$BODY" | jq -Rs .),
|
||||||
|
\"draft\": false,
|
||||||
|
\"prerelease\": $([ "${TAG#*-}" != "${TAG}" ] && echo true || echo false)
|
||||||
|
}"
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to SheetHappens are documented here.
|
||||||
|
Each release links to the full diff for detailed context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [v0.1.7] — 2026-06-22
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Revert splash screen overrides that caused Android crash-to-desktop — expo-router already manages the splash lifecycle internally
|
||||||
|
- Remove redundant `SafeAreaProvider` wrapper (expo-router's `ExpoRoot` already injects it)
|
||||||
|
|
||||||
|
[View changes](https://github.com/pirona/SheetHappens/compare/v0.1.6...v0.1.7)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [v0.1.6] — 2026-06-22
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Prevent black screen on launch by holding splash until SecureStore settings load
|
||||||
|
- Add `splash.backgroundColor` to `app.json` to match app theme on Android
|
||||||
|
|
||||||
|
[View changes](https://github.com/pirona/SheetHappens/compare/v0.1.5...v0.1.6)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [v0.1.5] — 2026-06-22
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fix signed APK release: inject signing secrets via env vars, write `signing.env` with `printf %q` to handle special characters
|
||||||
|
- Add `signing.env` verification step in CI
|
||||||
|
|
||||||
|
[View changes](https://github.com/pirona/SheetHappens/compare/v0.1.0...v0.1.5)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [v0.1.0] — Initial release
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Incident log with SQLite persistence
|
||||||
|
- Voice input via `expo-speech-recognition`
|
||||||
|
- Edit mode, service autocomplete
|
||||||
|
- Dark theme (NativeWind + expo-router)
|
||||||
|
- Onboarding flow
|
||||||
|
- Signed APK via GitHub Actions CI
|
||||||
@@ -5,12 +5,13 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
"backgroundColor": "#0f1117",
|
"icon": "./assets/icon.png",
|
||||||
|
"backgroundColor": "#1B2433",
|
||||||
"scheme": "sheethappens",
|
"scheme": "sheethappens",
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/adaptive-icon.png",
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
"backgroundColor": "#0f1117"
|
"backgroundColor": "#1B2433"
|
||||||
},
|
},
|
||||||
"package": "fr.gyozamancave.sheethappens",
|
"package": "fr.gyozamancave.sheethappens",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
+15
-1
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { Pressable, Text } from "react-native";
|
||||||
import { Stack, router } from "expo-router";
|
import { Stack, router } from "expo-router";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
@@ -28,7 +29,20 @@ export default function RootLayout() {
|
|||||||
animation: "slide_from_right",
|
animation: "slide_from_right",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="index" options={{ title: "SheetHappens" }} />
|
<Stack.Screen
|
||||||
|
name="index"
|
||||||
|
options={{
|
||||||
|
title: "SheetHappens",
|
||||||
|
headerRight: () => (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => router.push("/settings")}
|
||||||
|
style={{ paddingLeft: 12, paddingRight: 4, paddingVertical: 6 }}
|
||||||
|
>
|
||||||
|
<Text style={{ color: Colors.primary, fontSize: 22, lineHeight: 26 }}>⚙</Text>
|
||||||
|
</Pressable>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Stack.Screen name="new" options={{ title: "New Incident", presentation: "modal" }} />
|
<Stack.Screen name="new" options={{ title: "New Incident", presentation: "modal" }} />
|
||||||
<Stack.Screen name="incident/[id]" options={{ title: "Incident" }} />
|
<Stack.Screen name="incident/[id]" options={{ title: "Incident" }} />
|
||||||
<Stack.Screen name="settings" options={{ title: "Settings" }} />
|
<Stack.Screen name="settings" options={{ title: "Settings" }} />
|
||||||
|
|||||||
+86
-14
@@ -10,9 +10,10 @@ import {
|
|||||||
import { useLocalSearchParams, router, useNavigation } from "expo-router";
|
import { useLocalSearchParams, router, useNavigation } from "expo-router";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import * as SecureStore from "expo-secure-store";
|
import * as SecureStore from "expo-secure-store";
|
||||||
import { getIncidentById, updateIncident } from "@/lib/db";
|
import { getIncidentById, updateIncident, deleteIncident } from "@/lib/db";
|
||||||
import { renderMarkdown } from "@/lib/markdown";
|
import { renderMarkdown } from "@/lib/markdown";
|
||||||
import { pushIncidentToGitea } from "@/lib/gitea";
|
import { pushIncidentToGit } from "@/lib/git";
|
||||||
|
import type { GitProvider } from "@/lib/git";
|
||||||
import type { Incident } from "@/types/incident";
|
import type { Incident } from "@/types/incident";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
|
|
||||||
@@ -53,34 +54,88 @@ export default function IncidentDetailScreen() {
|
|||||||
}
|
}
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
if (!incident) return;
|
||||||
|
Alert.alert(
|
||||||
|
"Delete incident?",
|
||||||
|
"This action cannot be undone.",
|
||||||
|
[
|
||||||
|
{ text: "Cancel", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Delete",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: async () => {
|
||||||
|
await deleteIncident(incident.id);
|
||||||
|
router.replace("/");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleResolve() {
|
async function handleResolve() {
|
||||||
if (!incident) return;
|
if (!incident) return;
|
||||||
await updateIncident(incident.id, { status: "resolved" });
|
await updateIncident(incident.id, { status: "resolved" });
|
||||||
setIncident({ ...incident, status: "resolved" });
|
setIncident({ ...incident, status: "resolved" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGiteaPush() {
|
async function handleGitPush() {
|
||||||
if (!incident) return;
|
if (!incident) return;
|
||||||
const [url, token, owner, repo, template] = await Promise.all([
|
const [provider, template, giteaUrl, giteaToken, giteaOwner, giteaRepo, githubToken, githubOwner, githubRepo, gitlabUrl, gitlabToken, gitlabOwner, gitlabRepo] =
|
||||||
|
await Promise.all([
|
||||||
|
SecureStore.getItemAsync("git_provider"),
|
||||||
|
SecureStore.getItemAsync("md_template"),
|
||||||
SecureStore.getItemAsync("gitea_url"),
|
SecureStore.getItemAsync("gitea_url"),
|
||||||
SecureStore.getItemAsync("gitea_token"),
|
SecureStore.getItemAsync("gitea_token"),
|
||||||
SecureStore.getItemAsync("gitea_owner"),
|
SecureStore.getItemAsync("gitea_owner"),
|
||||||
SecureStore.getItemAsync("gitea_repo"),
|
SecureStore.getItemAsync("gitea_repo"),
|
||||||
SecureStore.getItemAsync("md_template"),
|
SecureStore.getItemAsync("github_token"),
|
||||||
|
SecureStore.getItemAsync("github_owner"),
|
||||||
|
SecureStore.getItemAsync("github_repo"),
|
||||||
|
SecureStore.getItemAsync("gitlab_url"),
|
||||||
|
SecureStore.getItemAsync("gitlab_token"),
|
||||||
|
SecureStore.getItemAsync("gitlab_owner"),
|
||||||
|
SecureStore.getItemAsync("gitlab_repo"),
|
||||||
]);
|
]);
|
||||||
if (!url || !token || !owner || !repo) {
|
|
||||||
Alert.alert("Gitea not configured", "Set up Gitea in Settings first.");
|
const p = (provider ?? "gitea") as GitProvider;
|
||||||
|
|
||||||
|
let url: string | undefined;
|
||||||
|
let token: string | null;
|
||||||
|
let owner: string | null;
|
||||||
|
let repo: string | null;
|
||||||
|
|
||||||
|
if (p === "gitea") {
|
||||||
|
url = giteaUrl ?? undefined;
|
||||||
|
token = giteaToken;
|
||||||
|
owner = giteaOwner;
|
||||||
|
repo = giteaRepo;
|
||||||
|
} else if (p === "github") {
|
||||||
|
token = githubToken;
|
||||||
|
owner = githubOwner;
|
||||||
|
repo = githubRepo;
|
||||||
|
} else {
|
||||||
|
url = gitlabUrl ?? undefined;
|
||||||
|
token = gitlabToken;
|
||||||
|
owner = gitlabOwner;
|
||||||
|
repo = gitlabRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsUrl = p === "gitea" || p === "gitlab";
|
||||||
|
if (!token || !owner || !repo || (needsUrl && !url)) {
|
||||||
|
Alert.alert("Git not configured", `Set up ${p.charAt(0).toUpperCase() + p.slice(1)} in Settings first.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPushing(true);
|
setPushing(true);
|
||||||
const result = await pushIncidentToGitea(
|
const result = await pushIncidentToGit(
|
||||||
incident,
|
incident,
|
||||||
{ url, token, owner, repo },
|
{ provider: p, url, token, owner, repo },
|
||||||
template ?? undefined
|
template ?? undefined
|
||||||
);
|
);
|
||||||
setPushing(false);
|
setPushing(false);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
Alert.alert("Pushed", result.url ? `${result.url}` : "Push successful.");
|
Alert.alert("Pushed", result.url ?? "Push successful.");
|
||||||
} else {
|
} else {
|
||||||
Alert.alert("Push failed", result.error ?? "Unknown error");
|
Alert.alert("Push failed", result.error ?? "Unknown error");
|
||||||
}
|
}
|
||||||
@@ -117,7 +172,7 @@ export default function IncidentDetailScreen() {
|
|||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
incident.status === "open" ? "#f59e0b20" : "#22c55e20",
|
incident.status === "open" ? "#f59e0b20" : "#88D65620",
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
@@ -188,7 +243,7 @@ export default function IncidentDetailScreen() {
|
|||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleResolve}
|
onPress={handleResolve}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#22c55e20",
|
backgroundColor: "#88D65620",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: Colors.success,
|
borderColor: Colors.success,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
@@ -219,7 +274,7 @@ export default function IncidentDetailScreen() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleGiteaPush}
|
onPress={handleGitPush}
|
||||||
disabled={pushing}
|
disabled={pushing}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: pushing ? Colors.surface2 : Colors.surface,
|
backgroundColor: pushing ? Colors.surface2 : Colors.surface,
|
||||||
@@ -237,7 +292,7 @@ export default function IncidentDetailScreen() {
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{pushing ? "Pushing…" : "Push to Gitea"}
|
{pushing ? "Pushing…" : "Push to Git"}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
@@ -256,6 +311,23 @@ export default function IncidentDetailScreen() {
|
|||||||
Edit
|
Edit
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
|
<Pressable
|
||||||
|
onPress={handleDelete}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#ef444410",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ef4444",
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 14,
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: "#ef4444", fontWeight: "600", fontSize: 15 }}>
|
||||||
|
Delete Incident
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
|||||||
+7
-5
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
Pressable,
|
Pressable,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { router } from "expo-router";
|
import { router, useFocusEffect } from "expo-router";
|
||||||
import { getIncidents } from "@/lib/db";
|
import { getIncidents } from "@/lib/db";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
import type { Incident } from "@/types/incident";
|
import type { Incident } from "@/types/incident";
|
||||||
@@ -26,9 +26,11 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
}, [ready, settings.homeView]);
|
}, [ready, settings.homeView]);
|
||||||
|
|
||||||
useEffect(() => {
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
loadIncidents();
|
loadIncidents();
|
||||||
}, []);
|
}, [])
|
||||||
|
);
|
||||||
|
|
||||||
async function loadIncidents() {
|
async function loadIncidents() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -98,7 +100,7 @@ export default function HomeScreen() {
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
item.status === "open"
|
item.status === "open"
|
||||||
? "#f59e0b20"
|
? "#f59e0b20"
|
||||||
: "#22c55e20",
|
: "#88D65620",
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
|
|||||||
+22
-2
@@ -10,6 +10,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { router, useLocalSearchParams, useNavigation } from "expo-router";
|
import { router, useLocalSearchParams, useNavigation } from "expo-router";
|
||||||
|
import * as SecureStore from "expo-secure-store";
|
||||||
import {
|
import {
|
||||||
createIncident,
|
createIncident,
|
||||||
updateIncident,
|
updateIncident,
|
||||||
@@ -46,7 +47,15 @@ const LABEL_STYLE = {
|
|||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type VoiceField = "title" | "symptom" | "rootCause" | "fix";
|
type VoiceField = "title" | "service" | "symptom" | "rootCause" | "fix";
|
||||||
|
|
||||||
|
function incrementTemplate(template: string): string {
|
||||||
|
const match = template.match(/^([\s\S]*?)(\d+)$/);
|
||||||
|
if (!match) return template;
|
||||||
|
const [, prefix, numStr] = match;
|
||||||
|
const next = (parseInt(numStr, 10) + 1).toString().padStart(numStr.length, "0");
|
||||||
|
return prefix + next;
|
||||||
|
}
|
||||||
|
|
||||||
export default function NewIncidentScreen() {
|
export default function NewIncidentScreen() {
|
||||||
const { editId } = useLocalSearchParams<{ editId?: string }>();
|
const { editId } = useLocalSearchParams<{ editId?: string }>();
|
||||||
@@ -74,6 +83,13 @@ export default function NewIncidentScreen() {
|
|||||||
getDistinctServices().then(setKnownServices);
|
getDistinctServices().then(setKnownServices);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editId) return;
|
||||||
|
SecureStore.getItemAsync("pref_title_template").then((tpl) => {
|
||||||
|
if (tpl) setForm((f) => ({ ...f, title: tpl }));
|
||||||
|
});
|
||||||
|
}, [editId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editId) return;
|
if (!editId) return;
|
||||||
navigation.setOptions({ title: "Edit Incident" });
|
navigation.setOptions({ title: "Edit Incident" });
|
||||||
@@ -144,6 +160,10 @@ export default function NewIncidentScreen() {
|
|||||||
await updateIncident(editId, form);
|
await updateIncident(editId, form);
|
||||||
} else {
|
} else {
|
||||||
await createIncident(form);
|
await createIncident(form);
|
||||||
|
const tpl = await SecureStore.getItemAsync("pref_title_template");
|
||||||
|
if (tpl) {
|
||||||
|
await SecureStore.setItemAsync("pref_title_template", incrementTemplate(tpl));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
router.back();
|
router.back();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -224,7 +244,7 @@ export default function NewIncidentScreen() {
|
|||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text style={[LABEL_STYLE, { marginBottom: 6 }]}>Service</Text>
|
{labelRow("Service", "service")}
|
||||||
<TextInput
|
<TextInput
|
||||||
style={FIELD_STYLE}
|
style={FIELD_STYLE}
|
||||||
value={form.service}
|
value={form.service}
|
||||||
|
|||||||
+319
-93
@@ -6,22 +6,33 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
Switch,
|
Switch,
|
||||||
Pressable,
|
Pressable,
|
||||||
Alert,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import * as SecureStore from "expo-secure-store";
|
import * as SecureStore from "expo-secure-store";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
import { DEFAULT_TEMPLATE } from "@/lib/markdown";
|
import { DEFAULT_TEMPLATE } from "@/lib/markdown";
|
||||||
|
import type { GitProvider } from "@/lib/git";
|
||||||
|
|
||||||
const KEYS = {
|
const KEYS = {
|
||||||
INPUT_MODE: "pref_input_mode",
|
INPUT_MODE: "pref_input_mode",
|
||||||
HOME_VIEW: "pref_home_view",
|
HOME_VIEW: "pref_home_view",
|
||||||
AI_ENABLED: "pref_ai_enabled",
|
AI_ENABLED: "pref_ai_enabled",
|
||||||
AI_KEY: "pref_ai_key",
|
AI_KEY: "pref_ai_key",
|
||||||
|
AI_BASE_URL: "pref_ai_base_url",
|
||||||
|
AI_MODEL: "pref_ai_model",
|
||||||
|
TITLE_TEMPLATE: "pref_title_template",
|
||||||
|
MD_TEMPLATE: "md_template",
|
||||||
|
GIT_PROVIDER: "git_provider",
|
||||||
GITEA_URL: "gitea_url",
|
GITEA_URL: "gitea_url",
|
||||||
GITEA_TOKEN: "gitea_token",
|
GITEA_TOKEN: "gitea_token",
|
||||||
GITEA_OWNER: "gitea_owner",
|
GITEA_OWNER: "gitea_owner",
|
||||||
GITEA_REPO: "gitea_repo",
|
GITEA_REPO: "gitea_repo",
|
||||||
MD_TEMPLATE: "md_template",
|
GITHUB_TOKEN: "github_token",
|
||||||
|
GITHUB_OWNER: "github_owner",
|
||||||
|
GITHUB_REPO: "github_repo",
|
||||||
|
GITLAB_URL: "gitlab_url",
|
||||||
|
GITLAB_TOKEN: "gitlab_token",
|
||||||
|
GITLAB_OWNER: "gitlab_owner",
|
||||||
|
GITLAB_REPO: "gitlab_repo",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const LABEL = {
|
const LABEL = {
|
||||||
@@ -53,58 +64,14 @@ const INPUT = {
|
|||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default function SettingsScreen() {
|
const PROVIDER_LABELS: Record<GitProvider, string> = {
|
||||||
const [inputMode, setInputMode] = useState<"voice" | "form">("form");
|
gitea: "Gitea",
|
||||||
const [homeView, setHomeView] = useState<"dashboard" | "capture">("dashboard");
|
github: "GitHub",
|
||||||
const [aiEnabled, setAiEnabled] = useState(false);
|
gitlab: "GitLab",
|
||||||
const [aiKey, setAiKey] = useState("");
|
};
|
||||||
const [giteaUrl, setGiteaUrl] = useState("");
|
|
||||||
const [giteaToken, setGiteaToken] = useState("");
|
|
||||||
const [giteaOwner, setGiteaOwner] = useState("");
|
|
||||||
const [giteaRepo, setGiteaRepo] = useState("");
|
|
||||||
const [mdTemplate, setMdTemplate] = useState(DEFAULT_TEMPLATE);
|
|
||||||
const [saved, setSaved] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Defined outside SettingsScreen — prevents remount on every rerender, which
|
||||||
(async () => {
|
// would cause TextInput to lose focus after each keystroke.
|
||||||
const [im, hv, ai, key, gu, gt, go, gr, tpl] = await Promise.all([
|
|
||||||
SecureStore.getItemAsync(KEYS.INPUT_MODE),
|
|
||||||
SecureStore.getItemAsync(KEYS.HOME_VIEW),
|
|
||||||
SecureStore.getItemAsync(KEYS.AI_ENABLED),
|
|
||||||
SecureStore.getItemAsync(KEYS.AI_KEY),
|
|
||||||
SecureStore.getItemAsync(KEYS.GITEA_URL),
|
|
||||||
SecureStore.getItemAsync(KEYS.GITEA_TOKEN),
|
|
||||||
SecureStore.getItemAsync(KEYS.GITEA_OWNER),
|
|
||||||
SecureStore.getItemAsync(KEYS.GITEA_REPO),
|
|
||||||
SecureStore.getItemAsync(KEYS.MD_TEMPLATE),
|
|
||||||
]);
|
|
||||||
if (im) setInputMode(im as "voice" | "form");
|
|
||||||
if (hv) setHomeView(hv as "dashboard" | "capture");
|
|
||||||
if (ai) setAiEnabled(ai === "true");
|
|
||||||
if (key) setAiKey(key);
|
|
||||||
if (gu) setGiteaUrl(gu);
|
|
||||||
if (gt) setGiteaToken(gt);
|
|
||||||
if (go) setGiteaOwner(go);
|
|
||||||
if (gr) setGiteaRepo(gr);
|
|
||||||
if (tpl) setMdTemplate(tpl);
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function handleSave() {
|
|
||||||
await Promise.all([
|
|
||||||
SecureStore.setItemAsync(KEYS.INPUT_MODE, inputMode),
|
|
||||||
SecureStore.setItemAsync(KEYS.HOME_VIEW, homeView),
|
|
||||||
SecureStore.setItemAsync(KEYS.AI_ENABLED, String(aiEnabled)),
|
|
||||||
SecureStore.setItemAsync(KEYS.AI_KEY, aiKey),
|
|
||||||
SecureStore.setItemAsync(KEYS.GITEA_URL, giteaUrl),
|
|
||||||
SecureStore.setItemAsync(KEYS.GITEA_TOKEN, giteaToken),
|
|
||||||
SecureStore.setItemAsync(KEYS.GITEA_OWNER, giteaOwner),
|
|
||||||
SecureStore.setItemAsync(KEYS.GITEA_REPO, giteaRepo),
|
|
||||||
SecureStore.setItemAsync(KEYS.MD_TEMPLATE, mdTemplate),
|
|
||||||
]);
|
|
||||||
setSaved(true);
|
|
||||||
setTimeout(() => setSaved(false), 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToggleRow({
|
function ToggleRow({
|
||||||
label,
|
label,
|
||||||
@@ -171,8 +138,8 @@ export default function SettingsScreen() {
|
|||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
color: value === opt.key ? "#fff" : Colors.text2,
|
color: value === opt.key ? Colors.bg : Colors.text2,
|
||||||
fontWeight: "600",
|
fontWeight: "700",
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -185,6 +152,142 @@ export default function SettingsScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Field({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
secure,
|
||||||
|
url,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
secure?: boolean;
|
||||||
|
url?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text style={LABEL}>{label}</Text>
|
||||||
|
<TextInput
|
||||||
|
style={INPUT}
|
||||||
|
value={value}
|
||||||
|
onChangeText={onChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={Colors.textDim}
|
||||||
|
secureTextEntry={secure}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardType={url ? "url" : "default"}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SettingsScreen() {
|
||||||
|
const [inputMode, setInputMode] = useState<"voice" | "form">("form");
|
||||||
|
const [homeView, setHomeView] = useState<"dashboard" | "capture">("dashboard");
|
||||||
|
const [aiEnabled, setAiEnabled] = useState(false);
|
||||||
|
const [aiKey, setAiKey] = useState("");
|
||||||
|
const [aiBaseUrl, setAiBaseUrl] = useState("");
|
||||||
|
const [aiModel, setAiModel] = useState("");
|
||||||
|
const [titleTemplate, setTitleTemplate] = useState("");
|
||||||
|
const [mdTemplate, setMdTemplate] = useState(DEFAULT_TEMPLATE);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
|
||||||
|
const [gitProvider, setGitProvider] = useState<GitProvider>("gitea");
|
||||||
|
const [giteaUrl, setGiteaUrl] = useState("");
|
||||||
|
const [giteaToken, setGiteaToken] = useState("");
|
||||||
|
const [giteaOwner, setGiteaOwner] = useState("");
|
||||||
|
const [giteaRepo, setGiteaRepo] = useState("");
|
||||||
|
const [githubToken, setGithubToken] = useState("");
|
||||||
|
const [githubOwner, setGithubOwner] = useState("");
|
||||||
|
const [githubRepo, setGithubRepo] = useState("");
|
||||||
|
const [gitlabUrl, setGitlabUrl] = useState("");
|
||||||
|
const [gitlabToken, setGitlabToken] = useState("");
|
||||||
|
const [gitlabOwner, setGitlabOwner] = useState("");
|
||||||
|
const [gitlabRepo, setGitlabRepo] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const [
|
||||||
|
im, hv, ai, key, aiUrl, aiMdl, titleTpl, tpl,
|
||||||
|
gp, gu, gt, go, gr,
|
||||||
|
ghu, ghown, ghrepo,
|
||||||
|
glu, glt, glo, glr,
|
||||||
|
] = await Promise.all([
|
||||||
|
SecureStore.getItemAsync(KEYS.INPUT_MODE),
|
||||||
|
SecureStore.getItemAsync(KEYS.HOME_VIEW),
|
||||||
|
SecureStore.getItemAsync(KEYS.AI_ENABLED),
|
||||||
|
SecureStore.getItemAsync(KEYS.AI_KEY),
|
||||||
|
SecureStore.getItemAsync(KEYS.AI_BASE_URL),
|
||||||
|
SecureStore.getItemAsync(KEYS.AI_MODEL),
|
||||||
|
SecureStore.getItemAsync(KEYS.TITLE_TEMPLATE),
|
||||||
|
SecureStore.getItemAsync(KEYS.MD_TEMPLATE),
|
||||||
|
SecureStore.getItemAsync(KEYS.GIT_PROVIDER),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITEA_URL),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITEA_TOKEN),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITEA_OWNER),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITEA_REPO),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITHUB_TOKEN),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITHUB_OWNER),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITHUB_REPO),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITLAB_URL),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITLAB_TOKEN),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITLAB_OWNER),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITLAB_REPO),
|
||||||
|
]);
|
||||||
|
if (im) setInputMode(im as "voice" | "form");
|
||||||
|
if (hv) setHomeView(hv as "dashboard" | "capture");
|
||||||
|
if (ai) setAiEnabled(ai === "true");
|
||||||
|
if (key) setAiKey(key);
|
||||||
|
if (aiUrl) setAiBaseUrl(aiUrl);
|
||||||
|
if (aiMdl) setAiModel(aiMdl);
|
||||||
|
if (titleTpl) setTitleTemplate(titleTpl);
|
||||||
|
if (tpl) setMdTemplate(tpl);
|
||||||
|
if (gp) setGitProvider(gp as GitProvider);
|
||||||
|
if (gu) setGiteaUrl(gu);
|
||||||
|
if (gt) setGiteaToken(gt);
|
||||||
|
if (go) setGiteaOwner(go);
|
||||||
|
if (gr) setGiteaRepo(gr);
|
||||||
|
if (ghu) setGithubToken(ghu);
|
||||||
|
if (ghown) setGithubOwner(ghown);
|
||||||
|
if (ghrepo) setGithubRepo(ghrepo);
|
||||||
|
if (glu) setGitlabUrl(glu);
|
||||||
|
if (glt) setGitlabToken(glt);
|
||||||
|
if (glo) setGitlabOwner(glo);
|
||||||
|
if (glr) setGitlabRepo(glr);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
await Promise.all([
|
||||||
|
SecureStore.setItemAsync(KEYS.INPUT_MODE, inputMode),
|
||||||
|
SecureStore.setItemAsync(KEYS.HOME_VIEW, homeView),
|
||||||
|
SecureStore.setItemAsync(KEYS.AI_ENABLED, String(aiEnabled)),
|
||||||
|
SecureStore.setItemAsync(KEYS.AI_KEY, aiKey),
|
||||||
|
SecureStore.setItemAsync(KEYS.AI_BASE_URL, aiBaseUrl),
|
||||||
|
SecureStore.setItemAsync(KEYS.AI_MODEL, aiModel),
|
||||||
|
SecureStore.setItemAsync(KEYS.TITLE_TEMPLATE, titleTemplate),
|
||||||
|
SecureStore.setItemAsync(KEYS.MD_TEMPLATE, mdTemplate),
|
||||||
|
SecureStore.setItemAsync(KEYS.GIT_PROVIDER, gitProvider),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITEA_URL, giteaUrl),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITEA_TOKEN, giteaToken),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITEA_OWNER, giteaOwner),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITEA_REPO, giteaRepo),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITHUB_TOKEN, githubToken),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITHUB_OWNER, githubOwner),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITHUB_REPO, githubRepo),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITLAB_URL, gitlabUrl),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITLAB_TOKEN, gitlabToken),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITLAB_OWNER, gitlabOwner),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITLAB_REPO, gitlabRepo),
|
||||||
|
]);
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={{ flex: 1, backgroundColor: Colors.bg }}
|
style={{ flex: 1, backgroundColor: Colors.bg }}
|
||||||
@@ -213,7 +316,48 @@ export default function SettingsScreen() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text style={SECTION_TITLE}>AI (optional)</Text>
|
<Field
|
||||||
|
label="Title Template"
|
||||||
|
value={titleTemplate}
|
||||||
|
onChange={setTitleTemplate}
|
||||||
|
placeholder="INC-001 (leave empty to disable)"
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: Colors.surface,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 10,
|
||||||
|
marginTop: -8,
|
||||||
|
marginBottom: 14,
|
||||||
|
borderLeftWidth: 3,
|
||||||
|
borderLeftColor: Colors.text2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: Colors.text2, fontSize: 12, lineHeight: 18 }}>
|
||||||
|
Pre-fills the Title field on new incidents. If the value ends with
|
||||||
|
digits, the number is auto-incremented after each save.{"\n"}
|
||||||
|
e.g. INC-001 → INC-002 → INC-003
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={SECTION_TITLE}>AI — Root Cause Assistant</Text>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: Colors.surface,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
marginBottom: 14,
|
||||||
|
borderLeftWidth: 3,
|
||||||
|
borderLeftColor: Colors.text2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: Colors.text2, fontSize: 13, lineHeight: 20 }}>
|
||||||
|
When enabled, a button appears on the capture form to suggest a root
|
||||||
|
cause and fix based on the incident title and symptom. Uses any
|
||||||
|
OpenAI-compatible API — set the base URL below to point at a local
|
||||||
|
Ollama instance, or leave empty for OpenAI (api.openai.com).
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
<ToggleRow
|
<ToggleRow
|
||||||
label="Enable AI assistance"
|
label="Enable AI assistance"
|
||||||
value={aiEnabled}
|
value={aiEnabled}
|
||||||
@@ -221,58 +365,140 @@ export default function SettingsScreen() {
|
|||||||
/>
|
/>
|
||||||
{aiEnabled && (
|
{aiEnabled && (
|
||||||
<>
|
<>
|
||||||
<Text style={LABEL}>API Key</Text>
|
<Field
|
||||||
<TextInput
|
label="API Base URL"
|
||||||
style={INPUT}
|
value={aiBaseUrl}
|
||||||
|
onChange={setAiBaseUrl}
|
||||||
|
placeholder="http://192.168.x.x:11434/v1 (empty = OpenAI)"
|
||||||
|
url
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Model"
|
||||||
|
value={aiModel}
|
||||||
|
onChange={setAiModel}
|
||||||
|
placeholder="gpt-4o-mini or llama3.2"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="API Key"
|
||||||
value={aiKey}
|
value={aiKey}
|
||||||
onChangeText={setAiKey}
|
onChange={setAiKey}
|
||||||
placeholder="sk-..."
|
placeholder="sk-... (leave empty for Ollama)"
|
||||||
placeholderTextColor={Colors.textDim}
|
secure
|
||||||
secureTextEntry
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text style={SECTION_TITLE}>Gitea</Text>
|
<Text style={SECTION_TITLE}>Git Repository</Text>
|
||||||
<Text style={LABEL}>Instance URL</Text>
|
|
||||||
<TextInput
|
<SegmentRow
|
||||||
style={INPUT}
|
label="Provider"
|
||||||
|
value={gitProvider}
|
||||||
|
onChange={(v) => setGitProvider(v as GitProvider)}
|
||||||
|
options={(["gitea", "github", "gitlab"] as GitProvider[]).map((p) => ({
|
||||||
|
key: p,
|
||||||
|
label: PROVIDER_LABELS[p],
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{gitProvider === "gitea" && (
|
||||||
|
<>
|
||||||
|
<Field
|
||||||
|
label="Instance URL"
|
||||||
value={giteaUrl}
|
value={giteaUrl}
|
||||||
onChangeText={setGiteaUrl}
|
onChange={setGiteaUrl}
|
||||||
placeholder="https://homegit.gyozamancave.fr"
|
placeholder="https://homegit.example.com"
|
||||||
placeholderTextColor={Colors.textDim}
|
url
|
||||||
autoCapitalize="none"
|
|
||||||
keyboardType="url"
|
|
||||||
/>
|
/>
|
||||||
<Text style={LABEL}>Token</Text>
|
<Field
|
||||||
<TextInput
|
label="Token"
|
||||||
style={INPUT}
|
|
||||||
value={giteaToken}
|
value={giteaToken}
|
||||||
onChangeText={setGiteaToken}
|
onChange={setGiteaToken}
|
||||||
placeholder="Bearer token"
|
placeholder="Bearer token"
|
||||||
placeholderTextColor={Colors.textDim}
|
secure
|
||||||
secureTextEntry
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
/>
|
||||||
<Text style={LABEL}>Owner</Text>
|
<Field
|
||||||
<TextInput
|
label="Owner"
|
||||||
style={INPUT}
|
|
||||||
value={giteaOwner}
|
value={giteaOwner}
|
||||||
onChangeText={setGiteaOwner}
|
onChange={setGiteaOwner}
|
||||||
placeholder="username or org"
|
placeholder="username or org"
|
||||||
placeholderTextColor={Colors.textDim}
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
/>
|
||||||
<Text style={LABEL}>Repository</Text>
|
<Field
|
||||||
<TextInput
|
label="Repository"
|
||||||
style={INPUT}
|
|
||||||
value={giteaRepo}
|
value={giteaRepo}
|
||||||
onChangeText={setGiteaRepo}
|
onChange={setGiteaRepo}
|
||||||
placeholder="incidents"
|
placeholder="incidents"
|
||||||
placeholderTextColor={Colors.textDim}
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{gitProvider === "github" && (
|
||||||
|
<>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: Colors.surface,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
marginBottom: 14,
|
||||||
|
borderLeftWidth: 3,
|
||||||
|
borderLeftColor: Colors.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: Colors.text2, fontSize: 13 }}>
|
||||||
|
Uses api.github.com — no instance URL required.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Field
|
||||||
|
label="Personal Access Token"
|
||||||
|
value={githubToken}
|
||||||
|
onChange={setGithubToken}
|
||||||
|
placeholder="ghp_..."
|
||||||
|
secure
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Owner (username or org)"
|
||||||
|
value={githubOwner}
|
||||||
|
onChange={setGithubOwner}
|
||||||
|
placeholder="your-username"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Repository"
|
||||||
|
value={githubRepo}
|
||||||
|
onChange={setGithubRepo}
|
||||||
|
placeholder="incidents"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{gitProvider === "gitlab" && (
|
||||||
|
<>
|
||||||
|
<Field
|
||||||
|
label="Instance URL"
|
||||||
|
value={gitlabUrl}
|
||||||
|
onChange={setGitlabUrl}
|
||||||
|
placeholder="https://gitlab.com"
|
||||||
|
url
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Personal Access Token"
|
||||||
|
value={gitlabToken}
|
||||||
|
onChange={setGitlabToken}
|
||||||
|
placeholder="glpat-..."
|
||||||
|
secure
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Namespace (user or group)"
|
||||||
|
value={gitlabOwner}
|
||||||
|
onChange={setGitlabOwner}
|
||||||
|
placeholder="your-username"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Repository"
|
||||||
|
value={gitlabRepo}
|
||||||
|
onChange={setGitlabRepo}
|
||||||
|
placeholder="incidents"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text style={SECTION_TITLE}>Markdown Template</Text>
|
<Text style={SECTION_TITLE}>Markdown Template</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -294,7 +520,7 @@ export default function SettingsScreen() {
|
|||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ color: "#fff", fontSize: 16, fontWeight: "700" }}>
|
<Text style={{ color: Colors.bg, fontSize: 16, fontWeight: "700" }}>
|
||||||
{saved ? "Saved ✓" : "Save Settings"}
|
{saved ? "Saved ✓" : "Save Settings"}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Executable
+189
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# build-apk.sh — Build a signed release APK locally without expo.dev / EAS.
|
||||||
|
#
|
||||||
|
# Prerequisites (first time only):
|
||||||
|
# 1. Generate a keystore:
|
||||||
|
# keytool -genkey -v -keystore ~/.config/sheethappens/sheethappens.jks \
|
||||||
|
# -alias sheethappens -keyalg RSA -keysize 2048 -validity 10000
|
||||||
|
# 2. Fill in signing credentials:
|
||||||
|
# mkdir -p ~/.config/sheethappens
|
||||||
|
# cat > ~/.config/sheethappens/signing.env <<EOF
|
||||||
|
# KEYSTORE_PATH=~/.config/sheethappens/sheethappens.jks
|
||||||
|
# KEYSTORE_ALIAS=sheethappens
|
||||||
|
# KEYSTORE_STORE_PASSWORD=your_store_password
|
||||||
|
# KEYSTORE_KEY_PASSWORD=your_key_password
|
||||||
|
# EOF
|
||||||
|
# chmod 600 ~/.config/sheethappens/signing.env
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./build-apk.sh # → dist/sheethappens-YYYYMMDD-HHMM.apk
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SIGNING_ENV="$HOME/.config/sheethappens/signing.env"
|
||||||
|
DIST_DIR="$SCRIPT_DIR/dist"
|
||||||
|
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[build]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
||||||
|
abort() { echo -e "${RED}[error]${NC} $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ─── 1. Signing credentials ────────────────────────────────────────────────
|
||||||
|
if [ ! -f "$SIGNING_ENV" ]; then
|
||||||
|
abort "Missing signing.env — see comments at top of this script."
|
||||||
|
fi
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$SIGNING_ENV"
|
||||||
|
|
||||||
|
[ -z "${KEYSTORE_PATH:-}" ] && abort "KEYSTORE_PATH not set in signing.env"
|
||||||
|
[ -z "${KEYSTORE_ALIAS:-}" ] && abort "KEYSTORE_ALIAS not set in signing.env"
|
||||||
|
[ -z "${KEYSTORE_STORE_PASSWORD:-}" ] && abort "KEYSTORE_STORE_PASSWORD not set in signing.env"
|
||||||
|
[ -z "${KEYSTORE_KEY_PASSWORD:-}" ] && abort "KEYSTORE_KEY_PASSWORD not set in signing.env"
|
||||||
|
|
||||||
|
KEYSTORE_PATH_EXPANDED="${KEYSTORE_PATH/#\$HOME/$HOME}"
|
||||||
|
KEYSTORE_PATH_EXPANDED="${KEYSTORE_PATH_EXPANDED/#~/$HOME}"
|
||||||
|
|
||||||
|
[ ! -f "$KEYSTORE_PATH_EXPANDED" ] && \
|
||||||
|
abort "Keystore not found: $KEYSTORE_PATH_EXPANDED"
|
||||||
|
|
||||||
|
# ─── 2. Java check ─────────────────────────────────────────────────────────
|
||||||
|
if [ -z "${JAVA_HOME:-}" ] || "$JAVA_HOME/bin/java" -version 2>&1 | grep -qE '"(2[5-9]|[3-9][0-9])\.'; then
|
||||||
|
if [ -d "$HOME/jdk21" ]; then
|
||||||
|
export JAVA_HOME="$HOME/jdk21"
|
||||||
|
export PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
info "Using local JDK 21: $JAVA_HOME"
|
||||||
|
else
|
||||||
|
abort "Java 25+ detected but Gradle only supports ≤ Java 24.\nInstall JDK 21 and set JAVA_HOME."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
info "Java: $("$JAVA_HOME/bin/java" -version 2>&1 | head -1)"
|
||||||
|
|
||||||
|
# ─── 3. Android SDK ────────────────────────────────────────────────────────
|
||||||
|
if [ -z "${ANDROID_HOME:-}" ]; then
|
||||||
|
for candidate in "$HOME/android-sdk" "$HOME/Android/Sdk" "/opt/android-sdk"; do
|
||||||
|
if [ -d "$candidate/platform-tools" ]; then
|
||||||
|
export ANDROID_HOME="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
[ -z "${ANDROID_HOME:-}" ] && abort "Android SDK not found. Set ANDROID_HOME."
|
||||||
|
export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin"
|
||||||
|
info "Android SDK: $ANDROID_HOME"
|
||||||
|
|
||||||
|
# ─── 4. expo prebuild ──────────────────────────────────────────────────────
|
||||||
|
info "Running expo prebuild (Android)…"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
npx expo prebuild --platform android --clean --no-install
|
||||||
|
|
||||||
|
# ─── 4.5. Patch native modules — 16KB page size (Android 15) ──────────────
|
||||||
|
info "Patching native modules for 16KB page size support (Android 15)…"
|
||||||
|
|
||||||
|
WORKLETS_CMAKE="$SCRIPT_DIR/node_modules/react-native-worklets/android/CMakeLists.txt"
|
||||||
|
REANIMATED_CMAKE="$SCRIPT_DIR/node_modules/react-native-reanimated/android/CMakeLists.txt"
|
||||||
|
|
||||||
|
if [ -f "$WORKLETS_CMAKE" ] && ! grep -q "max-page-size" "$WORKLETS_CMAKE"; then
|
||||||
|
printf '\ntarget_link_options(worklets PRIVATE "-Wl,-z,max-page-size=16384")\n' >> "$WORKLETS_CMAKE"
|
||||||
|
info "Patched react-native-worklets for 16KB pages"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$REANIMATED_CMAKE" ] && ! grep -q "max-page-size" "$REANIMATED_CMAKE"; then
|
||||||
|
printf '\ntarget_link_options(reanimated PRIVATE "-Wl,-z,max-page-size=16384")\n' >> "$REANIMATED_CMAKE"
|
||||||
|
info "Patched react-native-reanimated for 16KB pages"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── 4.6. Disable New Architecture (Android 15 compat) ────────────────────
|
||||||
|
GRADLE_PROPS_NEWARCH="$SCRIPT_DIR/android/gradle.properties"
|
||||||
|
if [ -f "$GRADLE_PROPS_NEWARCH" ]; then
|
||||||
|
sed -i 's/^newArchEnabled=true/newArchEnabled=false/' "$GRADLE_PROPS_NEWARCH"
|
||||||
|
if ! grep -q "^newArchEnabled=" "$GRADLE_PROPS_NEWARCH"; then
|
||||||
|
echo "newArchEnabled=false" >> "$GRADLE_PROPS_NEWARCH"
|
||||||
|
fi
|
||||||
|
info "New Architecture disabled in gradle.properties"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── 5. Patch build.gradle — inject release signingConfig ──────────────────
|
||||||
|
info "Patching android/app/build.gradle for release signing…"
|
||||||
|
|
||||||
|
python3 - "$SCRIPT_DIR/android/app/build.gradle" << 'PYEOF'
|
||||||
|
import sys, re
|
||||||
|
|
||||||
|
path = sys.argv[1]
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
if 'MYAPP_UPLOAD_STORE_FILE' in content:
|
||||||
|
print('[patch] build.gradle already patched, skipping.')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
release_signing = """\n release {
|
||||||
|
if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
|
||||||
|
storeFile file(MYAPP_UPLOAD_STORE_FILE)
|
||||||
|
storePassword MYAPP_UPLOAD_STORE_PASSWORD
|
||||||
|
keyAlias MYAPP_UPLOAD_KEY_ALIAS
|
||||||
|
keyPassword MYAPP_UPLOAD_KEY_PASSWORD
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
|
||||||
|
content = re.sub(
|
||||||
|
r'(signingConfigs\s*\{[\s\S]*?debug\s*\{[\s\S]*?\})',
|
||||||
|
r'\1' + release_signing,
|
||||||
|
content,
|
||||||
|
count=1
|
||||||
|
)
|
||||||
|
|
||||||
|
def replace_release_signing(m):
|
||||||
|
return m.group(0).replace('signingConfig signingConfigs.debug',
|
||||||
|
'signingConfig signingConfigs.release', 1)
|
||||||
|
|
||||||
|
content = re.sub(
|
||||||
|
r'release\s*\{[^}]*signingConfig\s+signingConfigs\.debug',
|
||||||
|
replace_release_signing,
|
||||||
|
content,
|
||||||
|
count=1,
|
||||||
|
flags=re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print('[patch] build.gradle patched for release signing.')
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# ─── 6. Inject signing props into gradle.properties ────────────────────────
|
||||||
|
info "Injecting signing credentials into gradle.properties…"
|
||||||
|
GRADLE_PROPS="$SCRIPT_DIR/android/gradle.properties"
|
||||||
|
|
||||||
|
sed -i '/^# --- sheethappens release signing/,/^# --- end signing/d' "$GRADLE_PROPS" 2>/dev/null || true
|
||||||
|
|
||||||
|
cat >> "$GRADLE_PROPS" << EOF
|
||||||
|
|
||||||
|
# --- sheethappens release signing (injected by build-apk.sh — wiped after build)
|
||||||
|
MYAPP_UPLOAD_STORE_FILE=$KEYSTORE_PATH_EXPANDED
|
||||||
|
MYAPP_UPLOAD_STORE_PASSWORD=$KEYSTORE_STORE_PASSWORD
|
||||||
|
MYAPP_UPLOAD_KEY_ALIAS=$KEYSTORE_ALIAS
|
||||||
|
MYAPP_UPLOAD_KEY_PASSWORD=$KEYSTORE_KEY_PASSWORD
|
||||||
|
# --- end signing
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ─── 7. Gradle build ───────────────────────────────────────────────────────
|
||||||
|
cd "$SCRIPT_DIR/android"
|
||||||
|
chmod +x gradlew
|
||||||
|
info "Building APK (release)…"
|
||||||
|
./gradlew assembleRelease --no-daemon
|
||||||
|
|
||||||
|
# ─── 8. Wipe credentials from gradle.properties ───────────────────────────
|
||||||
|
sed -i '/^# --- sheethappens release signing/,/^# --- end signing/d' "$GRADLE_PROPS"
|
||||||
|
info "Signing credentials wiped from gradle.properties."
|
||||||
|
|
||||||
|
# ─── 9. Copy to dist/ ─────────────────────────────────────────────────────
|
||||||
|
mkdir -p "$DIST_DIR"
|
||||||
|
TIMESTAMP="$(date +%Y%m%d-%H%M)"
|
||||||
|
OUTPUT="$DIST_DIR/sheethappens-$TIMESTAMP.apk"
|
||||||
|
cp "app/build/outputs/apk/release/app-release.apk" "$OUTPUT"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "Build complete!"
|
||||||
|
echo -e " ${GREEN}→ $OUTPUT${NC}"
|
||||||
|
echo ""
|
||||||
+12
-12
@@ -1,16 +1,16 @@
|
|||||||
export const Colors = {
|
export const Colors = {
|
||||||
bg: "#0f1117",
|
bg: "#1B2433",
|
||||||
surface: "#1a1d27",
|
surface: "#222D3D",
|
||||||
surface2: "#252836",
|
surface2: "#293648",
|
||||||
primary: "#6366f1",
|
primary: "#88D656",
|
||||||
primaryDim: "#4338ca",
|
primaryDim: "#5FA33A",
|
||||||
success: "#22c55e",
|
success: "#88D656",
|
||||||
warning: "#f59e0b",
|
warning: "#F5A623",
|
||||||
danger: "#ef4444",
|
danger: "#EF4444",
|
||||||
text1: "#f1f5f9",
|
text1: "#EEF2F5",
|
||||||
text2: "#94a3b8",
|
text2: "#8FA5BF",
|
||||||
textDim: "#64748b",
|
textDim: "#4E6480",
|
||||||
border: "#2d3147",
|
border: "#2E3F55",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const FontFamily = {
|
export const FontFamily = {
|
||||||
|
|||||||
+227
@@ -0,0 +1,227 @@
|
|||||||
|
import { incidentToFilename, renderMarkdown } from "./markdown";
|
||||||
|
import type { Incident } from "@/types/incident";
|
||||||
|
|
||||||
|
export type GitProvider = "gitea" | "github" | "gitlab";
|
||||||
|
|
||||||
|
export interface GitConfig {
|
||||||
|
provider: GitProvider;
|
||||||
|
url?: string; // required for gitea and self-hosted gitlab
|
||||||
|
token: string;
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PushResult {
|
||||||
|
success: boolean;
|
||||||
|
url?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
|
function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||||
|
return fetch(url, { ...init, signal: controller.signal }).finally(() =>
|
||||||
|
clearTimeout(timer)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pushIncidentToGit(
|
||||||
|
incident: Incident,
|
||||||
|
config: GitConfig,
|
||||||
|
markdownTemplate?: string
|
||||||
|
): Promise<PushResult> {
|
||||||
|
switch (config.provider) {
|
||||||
|
case "gitea":
|
||||||
|
return pushGitea(incident, config, markdownTemplate);
|
||||||
|
case "github":
|
||||||
|
return pushGitHub(incident, config, markdownTemplate);
|
||||||
|
case "gitlab":
|
||||||
|
return pushGitLab(incident, config, markdownTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushGitea(
|
||||||
|
incident: Incident,
|
||||||
|
config: GitConfig,
|
||||||
|
markdownTemplate?: string
|
||||||
|
): Promise<PushResult> {
|
||||||
|
if (!config.url) {
|
||||||
|
return { success: false, error: "Gitea instance URL is not set. Check Settings → Git Repository." };
|
||||||
|
}
|
||||||
|
if (!config.token) {
|
||||||
|
return { success: false, error: "Gitea token is not set. Check Settings → Git Repository." };
|
||||||
|
}
|
||||||
|
if (!config.owner || !config.repo) {
|
||||||
|
return { success: false, error: "Gitea owner or repository is not set. Check Settings → Git Repository." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const filepath = incidentToFilename(incident);
|
||||||
|
const content = renderMarkdown(incident, markdownTemplate);
|
||||||
|
const base64Content = btoa(unescape(encodeURIComponent(content)));
|
||||||
|
const apiBase = config.url.replace(/\/$/, "");
|
||||||
|
const endpoint = `${apiBase}/api/v1/repos/${config.owner}/${config.repo}/contents/${filepath}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = await fetchWithTimeout(endpoint, {
|
||||||
|
headers: { Authorization: `token ${config.token}` },
|
||||||
|
});
|
||||||
|
let sha: string | undefined;
|
||||||
|
if (existing.ok) {
|
||||||
|
const data = await existing.json() as { sha?: string };
|
||||||
|
sha = data.sha;
|
||||||
|
} else if (existing.status !== 404) {
|
||||||
|
const body = await existing.text();
|
||||||
|
return { success: false, error: `GET ${existing.status}: ${body.slice(0, 200)}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: Record<string, string> = {
|
||||||
|
message: `incident: ${incident.title}`,
|
||||||
|
content: base64Content,
|
||||||
|
};
|
||||||
|
if (sha) payload.sha = sha;
|
||||||
|
|
||||||
|
// POST creates, PUT updates — Gitea requires the correct verb
|
||||||
|
const res = await fetchWithTimeout(endpoint, {
|
||||||
|
method: sha ? "PUT" : "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${config.token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
return { success: false, error: `HTTP ${res.status}: ${body.slice(0, 300)}` };
|
||||||
|
}
|
||||||
|
const data = await res.json() as { content?: { html_url?: string } };
|
||||||
|
return { success: true, url: data.content?.html_url };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === "AbortError") {
|
||||||
|
return { success: false, error: `Request timed out after ${TIMEOUT_MS / 1000}s. Check that the Gitea URL is reachable.` };
|
||||||
|
}
|
||||||
|
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushGitHub(
|
||||||
|
incident: Incident,
|
||||||
|
config: GitConfig,
|
||||||
|
markdownTemplate?: string
|
||||||
|
): Promise<PushResult> {
|
||||||
|
if (!config.token) {
|
||||||
|
return { success: false, error: "GitHub token is not set. Check Settings → Git Repository." };
|
||||||
|
}
|
||||||
|
if (!config.owner || !config.repo) {
|
||||||
|
return { success: false, error: "GitHub owner or repository is not set. Check Settings → Git Repository." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const filepath = incidentToFilename(incident);
|
||||||
|
const content = renderMarkdown(incident, markdownTemplate);
|
||||||
|
const base64Content = btoa(unescape(encodeURIComponent(content)));
|
||||||
|
const endpoint = `https://api.github.com/repos/${config.owner}/${config.repo}/contents/${filepath}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = await fetchWithTimeout(endpoint, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.token}`,
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let sha: string | undefined;
|
||||||
|
if (existing.ok) {
|
||||||
|
const data = await existing.json() as { sha?: string };
|
||||||
|
sha = data.sha;
|
||||||
|
} else if (existing.status !== 404) {
|
||||||
|
const body = await existing.text();
|
||||||
|
return { success: false, error: `GET ${existing.status}: ${body.slice(0, 200)}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: Record<string, string> = {
|
||||||
|
message: `incident: ${incident.title}`,
|
||||||
|
content: base64Content,
|
||||||
|
};
|
||||||
|
if (sha) payload.sha = sha;
|
||||||
|
|
||||||
|
const res = await fetchWithTimeout(endpoint, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.token}`,
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
return { success: false, error: `HTTP ${res.status}: ${body.slice(0, 300)}` };
|
||||||
|
}
|
||||||
|
const data = await res.json() as { content?: { html_url?: string } };
|
||||||
|
return { success: true, url: data.content?.html_url };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === "AbortError") {
|
||||||
|
return { success: false, error: `Request timed out after ${TIMEOUT_MS / 1000}s.` };
|
||||||
|
}
|
||||||
|
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushGitLab(
|
||||||
|
incident: Incident,
|
||||||
|
config: GitConfig,
|
||||||
|
markdownTemplate?: string
|
||||||
|
): Promise<PushResult> {
|
||||||
|
if (!config.token) {
|
||||||
|
return { success: false, error: "GitLab token is not set. Check Settings → Git Repository." };
|
||||||
|
}
|
||||||
|
if (!config.owner || !config.repo) {
|
||||||
|
return { success: false, error: "GitLab namespace or repository is not set. Check Settings → Git Repository." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const filepath = incidentToFilename(incident);
|
||||||
|
const content = renderMarkdown(incident, markdownTemplate);
|
||||||
|
const base64Content = btoa(unescape(encodeURIComponent(content)));
|
||||||
|
const apiBase = (config.url ?? "https://gitlab.com").replace(/\/$/, "");
|
||||||
|
const projectId = encodeURIComponent(`${config.owner}/${config.repo}`);
|
||||||
|
const encodedPath = encodeURIComponent(filepath);
|
||||||
|
const endpoint = `${apiBase}/api/v4/projects/${projectId}/repository/files/${encodedPath}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = await fetchWithTimeout(`${endpoint}?ref=HEAD`, {
|
||||||
|
headers: { "PRIVATE-TOKEN": config.token },
|
||||||
|
});
|
||||||
|
const method = existing.ok ? "PUT" : "POST";
|
||||||
|
|
||||||
|
const res = await fetchWithTimeout(endpoint, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
"PRIVATE-TOKEN": config.token,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
branch: "main",
|
||||||
|
content: base64Content,
|
||||||
|
encoding: "base64",
|
||||||
|
commit_message: `incident: ${incident.title}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
return { success: false, error: `HTTP ${res.status}: ${body.slice(0, 300)}` };
|
||||||
|
}
|
||||||
|
const data = await res.json() as { file_path?: string };
|
||||||
|
const webUrl = data.file_path
|
||||||
|
? `${apiBase}/${config.owner}/${config.repo}/-/blob/main/${data.file_path}`
|
||||||
|
: undefined;
|
||||||
|
return { success: true, url: webUrl };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === "AbortError") {
|
||||||
|
return { success: false, error: `Request timed out after ${TIMEOUT_MS / 1000}s.` };
|
||||||
|
}
|
||||||
|
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+129
-143
@@ -19,7 +19,7 @@
|
|||||||
"expo-sqlite": "~56.0.5",
|
"expo-sqlite": "~56.0.5",
|
||||||
"expo-status-bar": "~56.0.4",
|
"expo-status-bar": "~56.0.4",
|
||||||
"nativewind": "~4.2.5",
|
"nativewind": "~4.2.5",
|
||||||
"react": "19.2.7",
|
"react": "19.2.3",
|
||||||
"react-native": "0.85.3",
|
"react-native": "0.85.3",
|
||||||
"react-native-gesture-handler": "~3.0.2",
|
"react-native-gesture-handler": "~3.0.2",
|
||||||
"react-native-reanimated": "~4.4.1",
|
"react-native-reanimated": "~4.4.1",
|
||||||
@@ -1154,99 +1154,6 @@
|
|||||||
"integrity": "sha512-IJkBtN1o8u9BW5fvSii1MyHPQ7Q0HxbWcVBvOrOzgMLpVtZw7R2w94wBTVR7kZwv3w1JNTESMmLA5Sqn1+Z36A==",
|
"integrity": "sha512-IJkBtN1o8u9BW5fvSii1MyHPQ7Q0HxbWcVBvOrOzgMLpVtZw7R2w94wBTVR7kZwv3w1JNTESMmLA5Sqn1+Z36A==",
|
||||||
"license": "MIT AND Apache-2.0"
|
"license": "MIT AND Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@expo/cli": {
|
|
||||||
"version": "56.1.16",
|
|
||||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.16.tgz",
|
|
||||||
"integrity": "sha512-VBQn0mqAwc67b9Cn0RVXyeodghomAx5xGRhA/bXaQzuxDjMQk0zIOb6pXMZX7yiIwJW66UZt/zQiJNSv6aWJYw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@expo/code-signing-certificates": "^0.0.6",
|
|
||||||
"@expo/config": "~56.0.9",
|
|
||||||
"@expo/config-plugins": "~56.0.9",
|
|
||||||
"@expo/devcert": "^1.2.1",
|
|
||||||
"@expo/env": "~2.3.0",
|
|
||||||
"@expo/image-utils": "^0.10.1",
|
|
||||||
"@expo/inline-modules": "^0.0.12",
|
|
||||||
"@expo/json-file": "^10.2.0",
|
|
||||||
"@expo/log-box": "^56.0.13",
|
|
||||||
"@expo/metro": "~56.0.0",
|
|
||||||
"@expo/metro-config": "~56.0.14",
|
|
||||||
"@expo/metro-file-map": "^56.0.3",
|
|
||||||
"@expo/osascript": "^2.6.0",
|
|
||||||
"@expo/package-manager": "^1.12.1",
|
|
||||||
"@expo/plist": "^0.7.0",
|
|
||||||
"@expo/prebuild-config": "^56.0.16",
|
|
||||||
"@expo/require-utils": "^56.1.3",
|
|
||||||
"@expo/router-server": "^56.0.14",
|
|
||||||
"@expo/schema-utils": "^56.0.0",
|
|
||||||
"@expo/spawn-async": "^1.8.0",
|
|
||||||
"@expo/ws-tunnel": "^2.0.0",
|
|
||||||
"@expo/xcpretty": "^4.4.4",
|
|
||||||
"@react-native/dev-middleware": "0.85.3",
|
|
||||||
"accepts": "^1.3.8",
|
|
||||||
"arg": "^5.0.2",
|
|
||||||
"bplist-creator": "0.1.0",
|
|
||||||
"bplist-parser": "^0.3.1",
|
|
||||||
"chalk": "^4.0.0",
|
|
||||||
"ci-info": "^3.3.0",
|
|
||||||
"compression": "^1.7.4",
|
|
||||||
"connect": "^3.7.0",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"dnssd-advertise": "^1.1.4",
|
|
||||||
"expo-server": "^56.0.5",
|
|
||||||
"fetch-nodeshim": "^0.4.10",
|
|
||||||
"getenv": "^2.0.0",
|
|
||||||
"glob": "^13.0.0",
|
|
||||||
"lan-network": "^0.2.1",
|
|
||||||
"multitars": "^1.0.0",
|
|
||||||
"node-forge": "^1.3.3",
|
|
||||||
"npm-package-arg": "^11.0.0",
|
|
||||||
"ora": "^3.4.0",
|
|
||||||
"picomatch": "^4.0.4",
|
|
||||||
"pretty-format": "^29.7.0",
|
|
||||||
"progress": "^2.0.3",
|
|
||||||
"prompts": "^2.3.2",
|
|
||||||
"resolve-from": "^5.0.0",
|
|
||||||
"semver": "^7.6.0",
|
|
||||||
"send": "^0.19.0",
|
|
||||||
"slugify": "^1.3.4",
|
|
||||||
"stacktrace-parser": "^0.1.10",
|
|
||||||
"structured-headers": "^0.4.1",
|
|
||||||
"terminal-link": "^2.1.1",
|
|
||||||
"toqr": "^0.1.1",
|
|
||||||
"wrap-ansi": "^7.0.0",
|
|
||||||
"ws": "^8.12.1",
|
|
||||||
"zod": "^3.25.76"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"expo-internal": "main.js"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*",
|
|
||||||
"expo-router": "*",
|
|
||||||
"react-native": "*"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"expo-router": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-native": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@expo/cli/node_modules/semver": {
|
|
||||||
"version": "7.8.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
|
||||||
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@expo/code-signing-certificates": {
|
"node_modules/@expo/code-signing-certificates": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz",
|
||||||
@@ -1659,9 +1566,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/prebuild-config/node_modules/semver": {
|
"node_modules/@expo/prebuild-config/node_modules/semver": {
|
||||||
"version": "7.8.4",
|
"version": "7.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -1689,40 +1596,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/router-server": {
|
|
||||||
"version": "56.0.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-56.0.14.tgz",
|
|
||||||
"integrity": "sha512-2UCTtZfcq1ZPgp3wk8/+sq9DvFI9UxrPr1jcEKMAF2DGAJLosnpc8GWNNg2hkjt6SHUOdFHIPxujWPYyho2y3A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "^4.3.4"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@expo/metro-runtime": "^56.0.15",
|
|
||||||
"expo": "*",
|
|
||||||
"expo-constants": "^56.0.18",
|
|
||||||
"expo-font": "^56.0.6",
|
|
||||||
"expo-router": "*",
|
|
||||||
"expo-server": "^56.0.5",
|
|
||||||
"react": "*",
|
|
||||||
"react-dom": "*",
|
|
||||||
"react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@expo/metro-runtime": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"expo-router": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-dom": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-server-dom-webpack": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@expo/schema-utils": {
|
"node_modules/@expo/schema-utils": {
|
||||||
"version": "56.0.1",
|
"version": "56.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-56.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-56.0.1.tgz",
|
||||||
@@ -4306,6 +4179,121 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo/node_modules/@expo/cli": {
|
||||||
|
"version": "56.1.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.16.tgz",
|
||||||
|
"integrity": "sha512-VBQn0mqAwc67b9Cn0RVXyeodghomAx5xGRhA/bXaQzuxDjMQk0zIOb6pXMZX7yiIwJW66UZt/zQiJNSv6aWJYw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/code-signing-certificates": "^0.0.6",
|
||||||
|
"@expo/config": "~56.0.9",
|
||||||
|
"@expo/config-plugins": "~56.0.9",
|
||||||
|
"@expo/devcert": "^1.2.1",
|
||||||
|
"@expo/env": "~2.3.0",
|
||||||
|
"@expo/image-utils": "^0.10.1",
|
||||||
|
"@expo/inline-modules": "^0.0.12",
|
||||||
|
"@expo/json-file": "^10.2.0",
|
||||||
|
"@expo/log-box": "^56.0.13",
|
||||||
|
"@expo/metro": "~56.0.0",
|
||||||
|
"@expo/metro-config": "~56.0.14",
|
||||||
|
"@expo/metro-file-map": "^56.0.3",
|
||||||
|
"@expo/osascript": "^2.6.0",
|
||||||
|
"@expo/package-manager": "^1.12.1",
|
||||||
|
"@expo/plist": "^0.7.0",
|
||||||
|
"@expo/prebuild-config": "^56.0.16",
|
||||||
|
"@expo/require-utils": "^56.1.3",
|
||||||
|
"@expo/router-server": "^56.0.14",
|
||||||
|
"@expo/schema-utils": "^56.0.0",
|
||||||
|
"@expo/spawn-async": "^1.8.0",
|
||||||
|
"@expo/ws-tunnel": "^2.0.0",
|
||||||
|
"@expo/xcpretty": "^4.4.4",
|
||||||
|
"@react-native/dev-middleware": "0.85.3",
|
||||||
|
"accepts": "^1.3.8",
|
||||||
|
"arg": "^5.0.2",
|
||||||
|
"bplist-creator": "0.1.0",
|
||||||
|
"bplist-parser": "^0.3.1",
|
||||||
|
"chalk": "^4.0.0",
|
||||||
|
"ci-info": "^3.3.0",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"connect": "^3.7.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"dnssd-advertise": "^1.1.4",
|
||||||
|
"expo-server": "^56.0.5",
|
||||||
|
"fetch-nodeshim": "^0.4.10",
|
||||||
|
"getenv": "^2.0.0",
|
||||||
|
"glob": "^13.0.0",
|
||||||
|
"lan-network": "^0.2.1",
|
||||||
|
"multitars": "^1.0.0",
|
||||||
|
"node-forge": "^1.3.3",
|
||||||
|
"npm-package-arg": "^11.0.0",
|
||||||
|
"ora": "^3.4.0",
|
||||||
|
"picomatch": "^4.0.4",
|
||||||
|
"pretty-format": "^29.7.0",
|
||||||
|
"progress": "^2.0.3",
|
||||||
|
"prompts": "^2.3.2",
|
||||||
|
"resolve-from": "^5.0.0",
|
||||||
|
"semver": "^7.6.0",
|
||||||
|
"send": "^0.19.0",
|
||||||
|
"slugify": "^1.3.4",
|
||||||
|
"stacktrace-parser": "^0.1.10",
|
||||||
|
"structured-headers": "^0.4.1",
|
||||||
|
"terminal-link": "^2.1.1",
|
||||||
|
"toqr": "^0.1.1",
|
||||||
|
"wrap-ansi": "^7.0.0",
|
||||||
|
"ws": "^8.12.1",
|
||||||
|
"zod": "^3.25.76"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"expo-internal": "main.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"expo-router": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"expo-router": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": {
|
||||||
|
"version": "56.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-56.0.14.tgz",
|
||||||
|
"integrity": "sha512-2UCTtZfcq1ZPgp3wk8/+sq9DvFI9UxrPr1jcEKMAF2DGAJLosnpc8GWNNg2hkjt6SHUOdFHIPxujWPYyho2y3A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@expo/metro-runtime": "^56.0.15",
|
||||||
|
"expo": "*",
|
||||||
|
"expo-constants": "^56.0.18",
|
||||||
|
"expo-font": "^56.0.6",
|
||||||
|
"expo-router": "*",
|
||||||
|
"expo-server": "^56.0.5",
|
||||||
|
"react": "*",
|
||||||
|
"react-dom": "*",
|
||||||
|
"react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@expo/metro-runtime": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"expo-router": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-server-dom-webpack": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo/node_modules/expo-modules-core": {
|
"node_modules/expo/node_modules/expo-modules-core": {
|
||||||
"version": "56.0.17",
|
"version": "56.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-56.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-56.0.17.tgz",
|
||||||
@@ -4359,8 +4347,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
||||||
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
},
|
},
|
||||||
@@ -6124,9 +6110,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/npm-package-arg/node_modules/semver": {
|
"node_modules/npm-package-arg/node_modules/semver": {
|
||||||
"version": "7.8.4",
|
"version": "7.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -6722,9 +6708,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.7",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -6762,16 +6748,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.2.7",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^19.2.7"
|
"react": "^19.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-fast-compare": {
|
"node_modules/react-fast-compare": {
|
||||||
|
|||||||
+5
-1
@@ -19,7 +19,7 @@
|
|||||||
"expo-sqlite": "~56.0.5",
|
"expo-sqlite": "~56.0.5",
|
||||||
"expo-status-bar": "~56.0.4",
|
"expo-status-bar": "~56.0.4",
|
||||||
"nativewind": "~4.2.5",
|
"nativewind": "~4.2.5",
|
||||||
"react": "19.2.7",
|
"react": "19.2.3",
|
||||||
"react-native": "0.85.3",
|
"react-native": "0.85.3",
|
||||||
"react-native-gesture-handler": "~3.0.2",
|
"react-native-gesture-handler": "~3.0.2",
|
||||||
"react-native-reanimated": "~4.4.1",
|
"react-native-reanimated": "~4.4.1",
|
||||||
@@ -27,6 +27,10 @@
|
|||||||
"react-native-screens": "~4.25.2",
|
"react-native-screens": "~4.25.2",
|
||||||
"tailwindcss": "~3.4.19"
|
"tailwindcss": "~3.4.19"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.0",
|
"@babel/core": "^7.25.0",
|
||||||
"@expo/metro-config": "~56.0.14",
|
"@expo/metro-config": "~56.0.14",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Exclude native build for packages not used in this project.
|
||||||
|
// react-native-gesture-handler detects their absence and falls back to its
|
||||||
|
// noreanimated codepath, so GestureHandlerRootView continues to work.
|
||||||
|
module.exports = {
|
||||||
|
dependencies: {
|
||||||
|
'react-native-reanimated': {
|
||||||
|
platforms: { android: null },
|
||||||
|
},
|
||||||
|
'react-native-worklets': {
|
||||||
|
platforms: { android: null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user