Merge branch 'streamyfin:master' into master

This commit is contained in:
Ryan
2025-01-06 20:28:12 +08:00
committed by GitHub
61 changed files with 1560 additions and 450 deletions

View File

@@ -5,7 +5,7 @@ import { apiAtom } from "@/providers/JellyfinProvider";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { writeToLog } from "@/utils/log";
import { msToSeconds, secondsToMs } from "@/utils/time";
import * as Haptics from "expo-haptics";
import { useHaptic } from "./useHaptic";
interface CreditTimestamps {
Introduction: {
@@ -29,6 +29,7 @@ export const useCreditSkipper = (
) => {
const [api] = useAtom(apiAtom);
const [showSkipCreditButton, setShowSkipCreditButton] = useState(false);
const lightHapticFeedback = useHaptic("light");
if (isVlc) {
currentTime = msToSeconds(currentTime);
@@ -79,7 +80,7 @@ export const useCreditSkipper = (
if (!creditTimestamps) return;
console.log(`Skipping credits to ${creditTimestamps.Credits.End}`);
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
lightHapticFeedback();
wrappedSeek(creditTimestamps.Credits.End);
setTimeout(() => {
play();

View File

@@ -6,7 +6,7 @@ import {
} from "@jellyfin/sdk/lib/generated-client";
import { useMemo } from "react";
// Used only for intial play settings.
// Used only for initial play settings.
const useDefaultPlaySettings = (
item: BaseItemDto,
settings: Settings | null

54
hooks/useHaptic.ts Normal file
View File

@@ -0,0 +1,54 @@
import { useCallback, useMemo } from "react";
import { Platform } from "react-native";
import * as Haptics from "expo-haptics";
import { useSettings } from "@/utils/atoms/settings";
export type HapticFeedbackType =
| "light"
| "medium"
| "heavy"
| "selection"
| "success"
| "warning"
| "error";
export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => {
const [settings] = useSettings();
const createHapticHandler = useCallback(
(type: Haptics.ImpactFeedbackStyle) => {
return Platform.OS === "web" ? () => {} : () => Haptics.impactAsync(type);
},
[]
);
const createNotificationFeedback = useCallback(
(type: Haptics.NotificationFeedbackType) => {
return Platform.OS === "web"
? () => {}
: () => Haptics.notificationAsync(type);
},
[]
);
const hapticHandlers = useMemo(
() => ({
light: createHapticHandler(Haptics.ImpactFeedbackStyle.Light),
medium: createHapticHandler(Haptics.ImpactFeedbackStyle.Medium),
heavy: createHapticHandler(Haptics.ImpactFeedbackStyle.Heavy),
selection: Platform.OS === "web" ? () => {} : Haptics.selectionAsync,
success: createNotificationFeedback(
Haptics.NotificationFeedbackType.Success
),
warning: createNotificationFeedback(
Haptics.NotificationFeedbackType.Warning
),
error: createNotificationFeedback(Haptics.NotificationFeedbackType.Error),
}),
[createHapticHandler, createNotificationFeedback]
);
if (settings?.disableHapticFeedback) {
return () => {};
}
return hapticHandlers[feedbackType];
};

View File

@@ -5,7 +5,7 @@ import { apiAtom } from "@/providers/JellyfinProvider";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { writeToLog } from "@/utils/log";
import { msToSeconds, secondsToMs } from "@/utils/time";
import * as Haptics from "expo-haptics";
import { useHaptic } from "./useHaptic";
interface IntroTimestamps {
EpisodeId: string;
@@ -33,6 +33,7 @@ export const useIntroSkipper = (
if (isVlc) {
currentTime = msToSeconds(currentTime);
}
const lightHapticFeedback = useHaptic("light");
const wrappedSeek = (seconds: number) => {
if (isVlc) {
@@ -78,7 +79,7 @@ export const useIntroSkipper = (
const skipIntro = useCallback(() => {
if (!introTimestamps) return;
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
lightHapticFeedback();
wrappedSeek(introTimestamps.IntroEnd);
setTimeout(() => {
play();

View File

@@ -28,6 +28,11 @@ import Issue from "@/utils/jellyseerr/server/entity/Issue";
import { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
import { writeErrorLog } from "@/utils/log";
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
import {
CombinedCredit,
PersonDetails,
} from "@/utils/jellyseerr/server/models/Person";
import { useQueryClient } from "@tanstack/react-query";
interface SearchParams {
query: string;
@@ -55,6 +60,8 @@ export enum Endpoints {
API_V1 = "/api/v1",
SEARCH = "/search",
REQUEST = "/request",
PERSON = "/person",
COMBINED_CREDITS = "/combined_credits",
MOVIE = "/movie",
RATINGS = "/ratings",
ISSUE = "/issue",
@@ -204,6 +211,27 @@ export class JellyseerrApi {
});
}
async personDetails(id: number | string): Promise<PersonDetails> {
return this.axios
?.get<PersonDetails>(Endpoints.API_V1 + Endpoints.PERSON + `/${id}`)
.then((response) => {
return response?.data;
});
}
async personCombinedCredits(id: number | string): Promise<CombinedCredit> {
return this.axios
?.get<CombinedCredit>(
Endpoints.API_V1 +
Endpoints.PERSON +
`/${id}` +
Endpoints.COMBINED_CREDITS
)
.then((response) => {
return response?.data;
});
}
async movieRatings(id: number) {
return this.axios
?.get<RTRating>(
@@ -238,14 +266,20 @@ export class JellyseerrApi {
});
}
tvStillImageProxy(path: string, width: number = 1920, quality: number = 75) {
return (
this.axios.defaults.baseURL +
`/_next/image?` +
new URLSearchParams(
`url=https://image.tmdb.org/t/p/original/${path}&w=${width}&q=${quality}`
).toString()
);
imageProxy(
path?: string,
tmdbPath: string = "original",
width: number = 1920,
quality: number = 75
) {
return path
? this.axios.defaults.baseURL +
`/_next/image?` +
new URLSearchParams(
`url=https://image.tmdb.org/t/p/${tmdbPath}/${path}&w=${width}&q=${quality}`
).toString()
: this.axios?.defaults.baseURL +
`/images/overseerr_poster_not_found_logo_top.png`;
}
async submitIssue(mediaId: number, issueType: IssueType, message: string) {
@@ -321,6 +355,7 @@ const jellyseerrUserAtom = atom(storage.get<JellyseerrUser>(JELLYSEERR_USER));
export const useJellyseerr = () => {
const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom);
const [settings, updateSettings] = useSettings();
const queryClient = useQueryClient();
const jellyseerrApi = useMemo(() => {
const cookies = storage.get<string[]>(JELLYSEERR_COOKIES);
@@ -338,12 +373,16 @@ export const useJellyseerr = () => {
const requestMedia = useCallback(
(title: string, request: MediaRequestBody, onSuccess?: () => void) => {
jellyseerrApi?.request?.(request)?.then((mediaRequest) => {
jellyseerrApi?.request?.(request)?.then(async (mediaRequest) => {
await queryClient.invalidateQueries({
queryKey: ["search", "jellyseerr"],
});
switch (mediaRequest.status) {
case MediaRequestStatus.PENDING:
case MediaRequestStatus.APPROVED:
toast.success(`Requested ${title}!`);
onSuccess?.()
onSuccess?.();
break;
case MediaRequestStatus.DECLINED:
toast.error(`You don't have permission to request!`);

View File

@@ -3,13 +3,14 @@ import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed";
import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQueryClient } from "@tanstack/react-query";
import * as Haptics from "expo-haptics";
import { useHaptic } from "./useHaptic";
import { useAtom } from "jotai";
export const useMarkAsPlayed = (item: BaseItemDto) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const queryClient = useQueryClient();
const lightHapticFeedback = useHaptic("light");
const invalidateQueries = () => {
const queriesToInvalidate = [
@@ -29,7 +30,7 @@ export const useMarkAsPlayed = (item: BaseItemDto) => {
};
const markAsPlayedStatus = async (played: boolean) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
lightHapticFeedback();
// Optimistic update
queryClient.setQueryData(