Merge branch 'main' into dependabot/npm_and_yarn/openai-6.15.0

This commit is contained in:
Violet Caulfield
2025-12-29 15:38:59 -06:00
committed by GitHub
11 changed files with 250 additions and 235 deletions
+2 -2
View File
@@ -43,7 +43,7 @@ jobs:
fi
- uses: actions/cache@v3
- uses: actions/cache@v5
with:
path: |
node_modules
@@ -68,7 +68,7 @@ jobs:
- name: 📦 Upload APK for testing
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: always()
with:
name: jellify-android-pr-${{ github.event.number }}-${{ env.VERSION_NUMBER }}
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
zip -r Jellify-Release-Simulator.zip Jellify.app
- name: 📦 Upload IPA for testing
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: always()
with:
name: jellify-ios-pr-${{ github.event.number }}-${{ env.VERSION_NUMBER }}
+7 -7
View File
@@ -24,7 +24,7 @@ jobs:
with:
bun-version: 1.3.4
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: |
node_modules
@@ -40,7 +40,7 @@ jobs:
java-version: '17'
- name: 🐘 Setup Gradle
uses: gradle/actions/setup-gradle@v3
uses: gradle/actions/setup-gradle@v5
- name: 🍎 Run bun init-android
run: bun i
@@ -58,7 +58,7 @@ jobs:
run: bun run android-build
- name: 📤 Upload Android Artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: android-artifacts
path: ./android/app/build/outputs/apk/release/*.apk
@@ -90,7 +90,7 @@ jobs:
with:
bun-version: 1.3.4
- uses: actions/cache@v3
- uses: actions/cache@v5
with:
path: |
node_modules
@@ -104,13 +104,13 @@ jobs:
run: export MAESTRO_VERSION=1.40.0; curl -Ls "https://get.maestro.mobile.dev" | bash
- name: Set up JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'zulu'
- name: ⬇️ Download Android Artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: android-artifacts
path: artifacts/
@@ -154,7 +154,7 @@ jobs:
DISCORD_WEBHOOK_URL: ${{ secrets.MAESTRO_WEBHOOK_RESULTS }}
- name: Store tests result
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v6
if: always()
with:
name: TestResult
+5 -5
View File
@@ -163,7 +163,7 @@ jobs:
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
- name: 📤 Upload Android Artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: android-artifacts
path: ./android/app/build/outputs/apk/release/*.apk
@@ -240,7 +240,7 @@ jobs:
MATCH_REPO_PAT: "anultravioletaurora:${{ secrets.SIGNING_REPO_PAT }}"
- name: 📤 Upload iOS Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: ios-artifacts
path: ./ios/Jellify.ipa
@@ -251,7 +251,7 @@ jobs:
runs-on: macos-15
steps:
- name: 🛒 Checkout Repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
token: ${{ secrets.SIGNING_REPO_PAT }}
@@ -290,14 +290,14 @@ jobs:
- name: ⬇️ Download Android Artifacts
if: ${{ github.event.inputs['build-platform'] == 'Android' || github.event.inputs['build-platform'] == 'Both' }}
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: android-artifacts
path: artifacts/
- name: ⬇️ Download iOS Artifacts
if: ${{ github.event.inputs['build-platform'] == 'iOS' || github.event.inputs['build-platform'] == 'Both' }}
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: ios-artifacts
path: artifacts/
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
bun-version: 1.3.4
- name: 📦 Cache dependencies
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.bun/install/cache
+6 -1
View File
@@ -133,7 +133,12 @@ export async function fetchPlaylistTracks(
Recursive: false,
Limit: ApiLimits.Library,
StartIndex: pageParam * ApiLimits.Library,
Fields: [ItemFields.MediaSources, ItemFields.ParentId, ItemFields.Path],
Fields: [
ItemFields.MediaSources,
ItemFields.ParentId,
ItemFields.Path,
ItemFields.SortName,
],
},
)
+196 -211
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { XStack, YStack, getToken } from 'tamagui'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, {
@@ -101,23 +101,23 @@ export default function SwipeableRow({
idRef.current = `swipeable-row-${Math.random().toString(36).slice(2)}`
}
const syncClosedState = useCallback(() => {
const syncClosedState = () => {
setIsMenuOpen(false)
menuOpenSV.value = false
notifySwipeableRowClosed(idRef.current!)
}, [menuOpenSV])
}
const close = useCallback(() => {
const close = () => {
syncClosedState()
cancelAnimation(tx)
tx.value = withTiming(0, { duration: 160, easing: Easing.out(Easing.cubic) })
}, [syncClosedState, tx])
}
const openMenu = useCallback(() => {
const openMenu = () => {
setIsMenuOpen(true)
menuOpenSV.value = true
notifySwipeableRowOpened(idRef.current!)
}, [menuOpenSV])
}
useEffect(() => {
registerSwipeableRow(idRef.current!, close)
@@ -130,215 +130,200 @@ export default function SwipeableRow({
const fgOpacity = useSharedValue(1.0)
const tapGesture = useMemo(() => {
// Reserve the right edge for per-row controls (e.g. three dots) by shrinking the tap area there
// so those controls can receive presses without being swallowed by the row tap gesture.
return Gesture.Tap()
.runOnJS(true)
.hitSlop({ right: -64 })
.maxDistance(2)
.onBegin(() => {
fgOpacity.set(0.5)
})
.onEnd((e, success) => {
// If a quick-action menu is open, row-level tap should NOT trigger onPress.
if (!isMenuOpen && onPress && success) {
/**
* Reserve the right edge for per-row controls (e.g. three dots) by shrinking the tap area there
* so those controls can receive presses without being swallowed by the row tap gesture.
*/
const tapGesture = Gesture.Tap()
.runOnJS(true)
.hitSlop({ right: -64 })
.maxDistance(2)
.onBegin(() => {
fgOpacity.set(0.5)
})
.onEnd((e, success) => {
// If a quick-action menu is open, row-level tap should NOT trigger onPress.
if (!isMenuOpen && onPress && success) {
triggerHaptic('impactLight')
onPress()
}
})
.onFinalize(() => {
fgOpacity.set(1.0)
})
const longPressGesture = Gesture.LongPress()
.runOnJS(true)
.onBegin(() => {
fgOpacity.set(0.5)
})
.onStart(() => {
if (onLongPress) {
triggerHaptic('effectDoubleClick')
onLongPress()
}
fgOpacity.set(1.0)
})
.onTouchesCancelled(() => {
fgOpacity.set(1.0)
})
const panGesture = Gesture.Pan()
.runOnJS(true)
.hitSlop({
/**
* Preserve Swipe to go back system gestures
*
* This was a value I saw ComputerJazz recommend in an issue on
* `react-native-draggable-flatlist`, figured it could serve as a good
* basis to start from and tune from there ~Vi
*
* {@link https://github.com/computerjazz}
* {@link https://github.com/computerjazz/react-native-draggable-flatlist/issues/336#issuecomment-970573916}
*/
left: -50,
})
.activeOffsetX([-15, 15])
.failOffsetY([-8, 8])
.onBegin(() => {
if (disabled) return
dragging.set(true)
fgOpacity.set(1.0)
})
.onUpdate((e) => {
if (disabled) return
const next = Math.max(Math.min(e.translationX, maxLeft), maxRight)
tx.value = next
})
.onEnd((e) => {
if (disabled) return
// Velocity-based assistance: fast flicks open even if displacement below threshold
const v = e.velocityX
const velocityTrigger = 800
if (tx.value > threshold) {
// Right swipe: show left quick actions if provided; otherwise trigger leftAction
if (leftActions && leftActions.length > 0) {
triggerHaptic('impactLight')
onPress()
// Snap open to expose quick actions, do not auto-trigger
cancelAnimation(tx)
tx.value = withTiming(maxLeft, {
duration: 140,
easing: Easing.out(Easing.cubic),
})
openMenu()
return
} else if (leftAction) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(
maxLeft,
{ duration: 140, easing: Easing.out(Easing.cubic) },
() => {
scheduleOnRN(leftAction.onTrigger)
cancelAnimation(tx)
tx.value = withTiming(0, {
duration: 160,
easing: Easing.out(Easing.cubic),
})
},
)
return
}
})
.onFinalize(() => {
fgOpacity.set(1.0)
})
}, [onPress, isMenuOpen])
const longPressGesture = useMemo(() => {
return Gesture.LongPress()
.runOnJS(true)
.onBegin(() => {
fgOpacity.set(0.5)
})
.onStart(() => {
if (onLongPress) {
triggerHaptic('effectDoubleClick')
onLongPress()
}
// Left swipe (quick actions)
if (tx.value < -Math.min(threshold, Math.abs(maxRight) / 2)) {
if (rightActions && rightActions.length > 0) {
triggerHaptic('impactLight')
// Snap open to expose quick actions, do not auto-trigger
cancelAnimation(tx)
tx.value = withTiming(maxRight, {
duration: 140,
easing: Easing.out(Easing.cubic),
})
openMenu()
return
} else if (rightAction) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(
maxRight,
{ duration: 140, easing: Easing.out(Easing.cubic) },
() => {
scheduleOnRN(rightAction.onTrigger)
cancelAnimation(tx)
tx.value = withTiming(0, {
duration: 160,
easing: Easing.out(Easing.cubic),
})
},
)
return
}
fgOpacity.set(1.0)
})
.onTouchesCancelled(() => {
fgOpacity.set(1.0)
})
}, [onLongPress])
const panGesture = useMemo(() => {
return Gesture.Pan()
.runOnJS(true)
.hitSlop({
/**
* Preserve Swipe to go back system gestures
*
* This was a value I saw ComputerJazz recommend in an issue on
* `react-native-draggable-flatlist`, figured it could serve as a good
* basis to start from and tune from there ~Vi
*
* {@link https://github.com/computerjazz}
* {@link https://github.com/computerjazz/react-native-draggable-flatlist/issues/336#issuecomment-970573916}
*/
left: -50,
})
.activeOffsetX([-15, 15])
.failOffsetY([-8, 8])
.onBegin(() => {
if (disabled) return
dragging.set(true)
fgOpacity.set(1.0)
})
.onUpdate((e) => {
if (disabled) return
const next = Math.max(Math.min(e.translationX, maxLeft), maxRight)
tx.value = next
})
.onEnd((e) => {
if (disabled) return
// Velocity-based assistance: fast flicks open even if displacement below threshold
const v = e.velocityX
const velocityTrigger = 800
if (tx.value > threshold) {
// Right swipe: show left quick actions if provided; otherwise trigger leftAction
if (leftActions && leftActions.length > 0) {
triggerHaptic('impactLight')
// Snap open to expose quick actions, do not auto-trigger
cancelAnimation(tx)
tx.value = withTiming(maxLeft, {
duration: 140,
easing: Easing.out(Easing.cubic),
})
openMenu()
return
} else if (leftAction) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(
maxLeft,
{ duration: 140, easing: Easing.out(Easing.cubic) },
() => {
scheduleOnRN(leftAction.onTrigger)
cancelAnimation(tx)
tx.value = withTiming(0, {
duration: 160,
easing: Easing.out(Easing.cubic),
})
},
)
return
}
}
// Velocity fallback (open quick actions if fast flick even without full displacement)
if (v > velocityTrigger && hasLeftSide) {
if (leftActions && leftActions.length > 0) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(maxLeft, {
duration: 140,
easing: Easing.out(Easing.cubic),
})
openMenu()
return
} else if (leftAction) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(
maxLeft,
{ duration: 140, easing: Easing.out(Easing.cubic) },
() => {
scheduleOnRN(leftAction.onTrigger)
cancelAnimation(tx)
tx.value = withTiming(0, {
duration: 160,
easing: Easing.out(Easing.cubic),
})
},
)
return
}
// Left swipe (quick actions)
if (tx.value < -Math.min(threshold, Math.abs(maxRight) / 2)) {
if (rightActions && rightActions.length > 0) {
triggerHaptic('impactLight')
// Snap open to expose quick actions, do not auto-trigger
cancelAnimation(tx)
tx.value = withTiming(maxRight, {
duration: 140,
easing: Easing.out(Easing.cubic),
})
openMenu()
return
} else if (rightAction) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(
maxRight,
{ duration: 140, easing: Easing.out(Easing.cubic) },
() => {
scheduleOnRN(rightAction.onTrigger)
cancelAnimation(tx)
tx.value = withTiming(0, {
duration: 160,
easing: Easing.out(Easing.cubic),
})
},
)
return
}
}
if (v < -velocityTrigger && hasRightSide) {
if (rightActions && rightActions.length > 0) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(maxRight, {
duration: 140,
easing: Easing.out(Easing.cubic),
})
openMenu()
return
} else if (rightAction) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(
maxRight,
{ duration: 140, easing: Easing.out(Easing.cubic) },
() => {
scheduleOnRN(rightAction.onTrigger)
cancelAnimation(tx)
tx.value = withTiming(0, {
duration: 160,
easing: Easing.out(Easing.cubic),
})
},
)
return
}
// Velocity fallback (open quick actions if fast flick even without full displacement)
if (v > velocityTrigger && hasLeftSide) {
if (leftActions && leftActions.length > 0) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(maxLeft, {
duration: 140,
easing: Easing.out(Easing.cubic),
})
openMenu()
return
} else if (leftAction) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(
maxLeft,
{ duration: 140, easing: Easing.out(Easing.cubic) },
() => {
scheduleOnRN(leftAction.onTrigger)
cancelAnimation(tx)
tx.value = withTiming(0, {
duration: 160,
easing: Easing.out(Easing.cubic),
})
},
)
return
}
}
if (v < -velocityTrigger && hasRightSide) {
if (rightActions && rightActions.length > 0) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(maxRight, {
duration: 140,
easing: Easing.out(Easing.cubic),
})
openMenu()
return
} else if (rightAction) {
triggerHaptic('impactLight')
cancelAnimation(tx)
tx.value = withTiming(
maxRight,
{ duration: 140, easing: Easing.out(Easing.cubic) },
() => {
scheduleOnRN(rightAction.onTrigger)
cancelAnimation(tx)
tx.value = withTiming(0, {
duration: 160,
easing: Easing.out(Easing.cubic),
})
},
)
return
}
}
tx.value = withTiming(0, { duration: 160, easing: Easing.out(Easing.cubic) })
syncClosedState()
})
.onFinalize(() => {
if (disabled) return
dragging.set(false)
})
}, [
disabled,
leftAction,
leftActions,
rightAction,
rightActions,
maxRight,
maxLeft,
openMenu,
syncClosedState,
triggerHaptic,
])
}
tx.value = withTiming(0, { duration: 160, easing: Easing.out(Easing.cubic) })
syncClosedState()
})
.onFinalize(() => {
if (disabled) return
dragging.set(false)
})
const fgStyle = useAnimatedStyle(() => ({
transform: [
+6 -3
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react'
import { getToken, Theme, useTheme, XStack, YStack } from 'tamagui'
import { getToken, getTokenValue, Theme, useTheme, XStack, YStack } from 'tamagui'
import { Text } from '../helpers/text'
import { RunTimeTicks } from '../helpers/time-codes'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
@@ -14,7 +14,7 @@ import navigationRef from '../../../../navigation'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../../../screens/types'
import ItemImage from './image'
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
import { useAddToQueue, useLoadNewQueue } from '../../../providers/Player/hooks/mutations'
import useStreamingDeviceProfile from '../../../stores/device-profile'
import { useDownloadedTrack } from '../../../api/queries/download'
@@ -292,7 +292,10 @@ export default function Track({
function HideableArtwork({ children }: { children: React.ReactNode }) {
const { tx } = useSwipeableRowContext()
// Hide artwork as soon as swiping starts (any non-zero tx)
const style = useAnimatedStyle(() => ({ opacity: tx.value === 0 ? 1 : 0 }))
const style = useAnimatedStyle(() => ({
marginHorizontal: 6,
opacity: withTiming(tx.value === 0 ? 1 : 0),
}))
return <Animated.View style={style}>{children}</Animated.View>
}
+10 -2
View File
@@ -106,13 +106,21 @@ export default function SongInfo({ swipeX }: SongInfoProps = {}): React.JSX.Elem
return (
<XStack>
<YStack justifyContent='flex-start' flex={1} gap={'$0.25'}>
<TextTicker {...TextTickerConfig} style={{ height: getToken('$9') }}>
<TextTicker
{...TextTickerConfig}
style={{ height: getToken('$9') }}
key={`${nowPlaying!.item.Id}-title`}
>
<Text bold fontSize={'$6'}>
{trackTitle}
</Text>
</TextTicker>
<TextTicker {...TextTickerConfig} style={{ height: getToken('$8') }}>
<TextTicker
{...TextTickerConfig}
style={{ height: getToken('$8') }}
key={`${nowPlaying!.item.Id}-artist`}
>
<Text fontSize={'$6'} color={'$color'} onPress={handleArtistPress}>
{nowPlaying?.artist ?? 'Unknown Artist'}
</Text>
+1 -1
View File
@@ -118,7 +118,7 @@ export default function Miniplayer(): React.JSX.Element {
<Animated.View
entering={FadeIn}
exiting={FadeOut}
key={`${nowPlaying!.item.AlbumId}-mini-player-song-info`}
key={`${nowPlaying!.item.Id}-mini-player-song-info`}
style={{
width: '100%',
}}
+15 -1
View File
@@ -225,12 +225,19 @@ export const useRemoveFromQueue = () => {
return async (index: number) => {
trigger('impactMedium')
TrackPlayer.remove([index])
await TrackPlayer.remove([index])
const prevQueue = usePlayerQueueStore.getState().queue
const newQueue = prevQueue.filter((_, i) => i !== index)
usePlayerQueueStore.getState().setQueue(newQueue)
// If queue is now empty, reset player state to hide miniplayer
if (newQueue.length === 0) {
usePlayerQueueStore.getState().setCurrentTrack(undefined)
usePlayerQueueStore.getState().setCurrentIndex(undefined)
await TrackPlayer.reset()
}
}
}
@@ -240,6 +247,13 @@ export const useRemoveUpcomingTracks = () => {
const newQueue = await TrackPlayer.getQueue()
usePlayerQueueStore.getState().setQueue(newQueue as JellifyTrack[])
// If queue is now empty, reset player state to hide miniplayer
if (newQueue.length === 0) {
usePlayerQueueStore.getState().setCurrentTrack(undefined)
usePlayerQueueStore.getState().setCurrentIndex(undefined)
await TrackPlayer.reset()
}
}
}