From eeab4d1221b0637624ed52b6c84d3dee92e38c40 Mon Sep 17 00:00:00 2001 From: tech08mag Date: Fri, 28 Aug 2026 23:45:36 +0200 Subject: [PATCH 1/2] - src/theme.tsx - Added light/dark themes, themeMode, settings sync functions - src/services/notifications.ts - Added useNotificationSettings hook, respects notifications toggle - app/(tabs)/settings.tsx - Added theme mode picker, fixed notifications toggle, added settings sync buttons --- carry-your-live/app/(tabs)/settings.tsx | 56 +++++++++- carry-your-live/src/services/notifications.ts | 2 +- carry-your-live/src/theme.tsx | 104 +++++++++++++----- 3 files changed, 130 insertions(+), 32 deletions(-) diff --git a/carry-your-live/app/(tabs)/settings.tsx b/carry-your-live/app/(tabs)/settings.tsx index 8112adc..c24f068 100644 --- a/carry-your-live/app/(tabs)/settings.tsx +++ b/carry-your-live/app/(tabs)/settings.tsx @@ -74,7 +74,9 @@ export default function SettingsScreen() { loadSettingsFromServer, } = useSettings(); const categories = useCategories(); - const [picker, setPicker] = useState(null); + const [picker, setPicker] = useState< + null | 'category' | 'sort' | 'reminder' | 'todoAhead' | 'themeMode' + >(null); const [reminderTagPicker, setReminderTagPicker] = useState(null); const [editingCategory, setEditingCategory] = useState(null); const [syncVisible, setSyncVisible] = useState(false); @@ -245,7 +247,7 @@ export default function SettingsScreen() { rightElement={ { + onValueChange={async value => { setNotifications(value); if (!value) { const { cancelAllReminders } = await import('@/services/notifications'); @@ -358,6 +360,15 @@ export default function SettingsScreen() { onPress={openCustomAccent} showChevron /> + Theme + setPicker('themeMode')} + showChevron + /> Data setSyncVisible(true)} showChevron /> + { + try { + await syncSettingsToServer(); + Alert.alert('Success', 'Settings synced to server'); + } catch { + Alert.alert('Error', 'Failed to sync settings'); + } + }} + showChevron + /> + { + try { + await loadSettingsFromServer(); + Alert.alert('Success', 'Settings loaded from server'); + } catch { + Alert.alert('Error', 'Failed to load settings'); + } + }} + showChevron + /> setPicker(null)} /> + setThemeMode(value as ThemeMode)} + onClose={() => setPicker(null)} + /> + 0.5 ? '#111111' : '#FFFFFF'; } -export function colors(accent: string = DEFAULT_ACCENT, mode: ThemeMode = 'dark', opacity: number = 1): ThemeColors { +export function colors( + accent: string = DEFAULT_ACCENT, + mode: ThemeMode = 'dark', + opacity: number = 1 +): ThemeColors { const softAccent = desaturate(accent, 0.35); - const accentWithOpacity = opacity < 1 ? `${opacity.toString(16).padStart(2, '0')}${accent.slice(1)}` : accent; - + const accentWithOpacity = + opacity < 1 ? `${opacity.toString(16).padStart(2, '0')}${accent.slice(1)}` : accent; + if (mode === 'light') { return { background: '#FFFFFF', @@ -144,7 +157,7 @@ export function colors(accent: string = DEFAULT_ACCENT, mode: ThemeMode = 'dark' info: '#1E88E5', }; } - + // dark mode return { background: '#121212', @@ -289,34 +302,52 @@ export function SettingsProvider({ children }: { children: ReactNode }) { STORAGE_KEYS.reminderPreference, '15' ); - const [tagReminderPreferences, setTagReminderPreferences] = useStoredSetting( - STORAGE_KEYS.tagReminderPreferences, - {} - ); + const [tagReminderPreferences, setTagReminderPreferences] = + useStoredSetting(STORAGE_KEYS.tagReminderPreferences, {}); const [apiUrl, setApiUrl] = useStoredSetting(API_URL_KEY, DEFAULT_API_BASE_URL); - const [accentColor, setAccentColor] = useStoredSetting(STORAGE_KEYS.accentColor, DEFAULT_ACCENT); - const [accentOpacity, setAccentOpacity] = useStoredSetting(STORAGE_KEYS.accentOpacity, DEFAULT_ACCENT_OPACITY); - const [todoAheadDays, setTodoAheadDays] = useStoredSetting(STORAGE_KEYS.todoAheadDays, DEFAULT_TODO_AHEAD_DAYS); - const [showCompleted, setShowCompleted] = useStoredSetting(STORAGE_KEYS.showCompleted, false); - const [deleteBySwipe, setDeleteBySwipe] = useStoredSetting(STORAGE_KEYS.deleteBySwipe, false); + const [accentColor, setAccentColor] = useStoredSetting( + STORAGE_KEYS.accentColor, + DEFAULT_ACCENT + ); + const [accentOpacity, setAccentOpacity] = useStoredSetting( + STORAGE_KEYS.accentOpacity, + DEFAULT_ACCENT_OPACITY + ); + const [todoAheadDays, setTodoAheadDays] = useStoredSetting( + STORAGE_KEYS.todoAheadDays, + DEFAULT_TODO_AHEAD_DAYS + ); + const [showCompleted, setShowCompleted] = useStoredSetting( + STORAGE_KEYS.showCompleted, + false + ); + const [deleteBySwipe, setDeleteBySwipe] = useStoredSetting( + STORAGE_KEYS.deleteBySwipe, + false + ); const [themeMode, setThemeMode, themeModeLoaded] = useStoredSetting( STORAGE_KEYS.themeMode, 'system' ); - const effectiveMode = themeMode === 'system' - ? (Platform.OS === 'web' ? 'dark' : 'dark') // Will be updated by useColorScheme in real app - : themeMode; + const effectiveMode: 'light' | 'dark' = + themeMode === 'system' + ? 'dark' // Will be updated by useColorScheme in real app + : themeMode; - const theme = useMemo(() => colors(accentColor, effectiveMode, accentOpacity), [accentColor, effectiveMode, accentOpacity]); + const theme = useMemo( + () => colors(accentColor, effectiveMode, accentOpacity), + [accentColor, effectiveMode, accentOpacity] + ); const syncSettingsToServer = useCallback(async () => { const { getAuthToken } = await import('@/services/auth'); const token = await getAuthToken(); if (!token) return; - + + const isDark = effectiveMode === 'dark'; const settings = { - darkMode: effectiveMode === 'dark' || (themeMode === 'system' && effectiveMode === 'dark'), + darkMode: isDark, defaultCategoryId, sortOrder: sortBy, reminderPreference, @@ -325,13 +356,23 @@ export function SettingsProvider({ children }: { children: ReactNode }) { deleteBySwipe, }; await pushServerSettings(apiUrl, token, settings); - }, [apiUrl, effectiveMode, themeMode, defaultCategoryId, sortBy, reminderPreference, todoAheadDays, showCompleted, deleteBySwipe]); + }, [ + apiUrl, + effectiveMode, + themeMode, + defaultCategoryId, + sortBy, + reminderPreference, + todoAheadDays, + showCompleted, + deleteBySwipe, + ]); const loadSettingsFromServer = useCallback(async () => { const { getAuthToken } = await import('@/services/auth'); const token = await getAuthToken(); if (!token) return; - + const serverSettings = await fetchServerSettings(apiUrl, token); if (!serverSettings) return; @@ -344,7 +385,16 @@ export function SettingsProvider({ children }: { children: ReactNode }) { if (serverSettings.todoAheadDays !== undefined) setTodoAheadDays(serverSettings.todoAheadDays); if (serverSettings.showCompleted !== undefined) setShowCompleted(serverSettings.showCompleted); if (serverSettings.deleteBySwipe !== undefined) setDeleteBySwipe(serverSettings.deleteBySwipe); - }, [apiUrl, setThemeMode, setDefaultCategoryId, setSortBy, setReminderPreference, setTodoAheadDays, setShowCompleted, setDeleteBySwipe]); + }, [ + apiUrl, + setThemeMode, + setDefaultCategoryId, + setSortBy, + setReminderPreference, + setTodoAheadDays, + setShowCompleted, + setDeleteBySwipe, + ]); const value = useMemo( () => ({ @@ -407,11 +457,7 @@ export function SettingsProvider({ children }: { children: ReactNode }) { ] ); - return ( - - {children} - - ); + return {children}; } export function useSettings() { @@ -422,4 +468,4 @@ export function useSettings() { return context; } -export * from './theme/tokens'; \ No newline at end of file +export * from './theme/tokens'; From 55e21c06d94d3086231b891ed75afe3eb461f8f4 Mon Sep 17 00:00:00 2001 From: tech08mag Date: Tue, 1 Sep 2026 21:22:48 +0200 Subject: [PATCH 2/2] new pipe --- .gitea/workflows/release.yml | 107 ++++++++++++++--------------------- 1 file changed, 44 insertions(+), 63 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index b046434..e22a1c9 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -2,24 +2,22 @@ name: Build and Release APK on: push: - branches: [main] + tags: + - 'v*' workflow_dispatch: env: ANDROID_HOME: ${{ runner.tool_cache }}/android-sdk ANDROID_SDK_ROOT: ${{ runner.tool_cache }}/android-sdk -permissions: - contents: write - jobs: build: runs-on: ubuntu-latest - outputs: - apk-path: ${{ steps.upload-apk.outputs.path }} steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Node uses: actions/setup-node@v4 @@ -43,7 +41,6 @@ jobs: unzip -q /tmp/cmdtools.zip -d "$ANDROID_HOME/cmdline-tools" mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest" fi - # Accept all licenses by writing known license hashes mkdir -p "$ANDROID_HOME/licenses" echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license" echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> "$ANDROID_HOME/licenses/android-sdk-license" @@ -78,63 +75,47 @@ jobs: cd carry-your-live/android ./gradlew assembleRelease - - name: Upload APK - id: upload-apk - uses: actions/upload-artifact@v3 - with: - name: carry-your-live-release - path: carry-your-live/android/app/build/outputs/release/app-release.apk - - release: - needs: build - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Download APK artifact - uses: actions/download-artifact@v3 - with: - name: carry-your-live-release - path: . - - - name: Compute next version - id: version + - name: Find APK + id: find-apk run: | - latest=$(git tag --list 'v[0-9]*' --sort=-v:refname | head -n 1) - if [ -z "$latest" ]; then - next="v1.0.0" - else - latest=${latest#v} - IFS='.' read -r major minor patch <<< "$latest" - next="v${major}.${minor}.$((patch + 1))" - fi - echo "next=${next}" >> "$GITHUB_OUTPUT" + APK_PATH=$(find carry-your-live/android/app/build/outputs -name "*.apk" -type f | head -n 1) + echo "apk_path=$APK_PATH" >> "$GITHUB_OUTPUT" + echo "Found APK at: $APK_PATH" - - name: Build changelog - id: changelog + - name: Create Gitea Release and Upload APK + if: startsWith(github.ref, 'refs/tags/v') + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: | - prev_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - if [ -z "$prev_tag" ]; then - log=$(git log --oneline --no-merges main | head -n 40) - else - log=$(git log --oneline --no-merges "$prev_tag"..main | head -n 40) + TAG_NAME="${GITHUB_REF#refs/tags/}" + REPO_OWNER="${GITHUB_REPOSITORY%/*}" + REPO_NAME="${GITHUB_REPOSITORY#*/}" + APK_PATH="${{ steps.find-apk.outputs.apk_path }}" + APK_NAME="carry-your-live-${TAG_NAME}.apk" + + # Create release using Gitea API + RESPONSE=$(curl -s -X POST "https://techmos.gitea.org/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"tag_name\": \"${TAG_NAME}\", + \"name\": \"${TAG_NAME}\", + \"body\": \"Automated release for ${TAG_NAME}\", + \"draft\": false, + \"prerelease\": false + }") + + RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id') + + if [ "$RELEASE_ID" = "null" ] || [ -z "$RELEASE_ID" ]; then + echo "Release might already exist, fetching existing release..." + RELEASE_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \ + "https://techmos.gitea.org/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${TAG_NAME}" | jq -r '.id') fi - { - echo "body<> "$GITHUB_OUTPUT" - - - name: Create release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.version.outputs.next }} - name: ${{ steps.version.outputs.next }} - body: ${{ steps.changelog.outputs.body }} - files: ./app-release.apk - generate_release_notes: false \ No newline at end of file + + # Upload APK as attachment + curl -s -X POST "https://techmos.gitea.org/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -F "attachment=@${APK_PATH};filename=${APK_NAME}" + + echo "APK uploaded to release ${TAG_NAME}" \ No newline at end of file