mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-07 04:53:01 +01:00
Compare commits
1 Commits
feat/count
...
renovate/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ca14a4746 |
6
.github/workflows/ci-codeql.yml
vendored
6
.github/workflows/ci-codeql.yml
vendored
@@ -30,13 +30,13 @@ jobs:
|
|||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: 🏁 Initialize CodeQL
|
- name: 🏁 Initialize CodeQL
|
||||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended,security-and-quality
|
queries: +security-extended,security-and-quality
|
||||||
|
|
||||||
- name: 🛠️ Autobuild
|
- name: 🛠️ Autobuild
|
||||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
|
|
||||||
- name: 🧪 Perform CodeQL Analysis
|
- name: 🧪 Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
|
|||||||
2
.github/workflows/trivy-scan.yml
vendored
2
.github/workflows/trivy-scan.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
output: trivy-results.sarif
|
output: trivy-results.sarif
|
||||||
|
|
||||||
- name: 📤 Upload results to code scanning
|
- name: 📤 Upload results to code scanning
|
||||||
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
with:
|
with:
|
||||||
sarif_file: trivy-results.sarif
|
sarif_file: trivy-results.sarif
|
||||||
category: trivy-fs
|
category: trivy-fs
|
||||||
|
|||||||
@@ -1,116 +1,43 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import React from "react";
|
import type React from "react";
|
||||||
import { Platform, View, type ViewStyle } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import { Text } from "@/components/common/Text";
|
|
||||||
import { scaleSize } from "@/utils/scaleSize";
|
|
||||||
|
|
||||||
const isAggregateType = (item: BaseItemDto) =>
|
|
||||||
item.Type === "Series" || item.Type === "BoxSet";
|
|
||||||
|
|
||||||
// TV sizes are scaled relative to a 1920×1080 reference (see scaleSize).
|
|
||||||
const tvBadgeBase: ViewStyle = {
|
|
||||||
position: "absolute",
|
|
||||||
top: scaleSize(8),
|
|
||||||
right: scaleSize(8),
|
|
||||||
height: scaleSize(28),
|
|
||||||
borderRadius: scaleSize(14),
|
|
||||||
backgroundColor: "rgba(255,255,255,0.92)",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mobile uses raw dp — no scaling.
|
|
||||||
const mobileBadgeBase: ViewStyle = {
|
|
||||||
position: "absolute",
|
|
||||||
top: 4,
|
|
||||||
right: 4,
|
|
||||||
height: 20,
|
|
||||||
borderRadius: 10,
|
|
||||||
backgroundColor: "#9333ea",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the unplayed-episode count badge for Series/BoxSet items that still
|
|
||||||
* have episodes left to watch. Returns null for non-aggregate types, fully
|
|
||||||
* watched items, or items with no unplayed count, so it is safe to mount
|
|
||||||
* unconditionally as an overlay (e.g. on top of the tvOS glass poster, where
|
|
||||||
* the watched checkmark is drawn natively and only the count needs RN).
|
|
||||||
*/
|
|
||||||
export const UnplayedCountBadge: React.FC<{ item: BaseItemDto }> = React.memo(
|
|
||||||
({ item }) => {
|
|
||||||
if (!isAggregateType(item)) return null;
|
|
||||||
if (item.UserData?.Played) return null;
|
|
||||||
const unplayed = item.UserData?.UnplayedItemCount ?? 0;
|
|
||||||
if (unplayed <= 0) return null;
|
|
||||||
|
|
||||||
if (Platform.isTV) {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
tvBadgeBase,
|
|
||||||
{ minWidth: scaleSize(28), paddingHorizontal: scaleSize(7) },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: scaleSize(15),
|
|
||||||
fontWeight: "700",
|
|
||||||
color: "black",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{unplayed}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[mobileBadgeBase, { minWidth: 20, paddingHorizontal: 5 }]}>
|
|
||||||
<Text style={{ fontSize: 12, fontWeight: "700", color: "white" }}>
|
|
||||||
{unplayed}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const WatchedIndicator: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
export const WatchedIndicator: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||||
const isMovieOrEpisode = item.Type === "Movie" || item.Type === "Episode";
|
|
||||||
const isAggregate = isAggregateType(item);
|
|
||||||
const isPlayed = item.UserData?.Played === true;
|
|
||||||
|
|
||||||
if (Platform.isTV) {
|
if (Platform.isTV) {
|
||||||
// Fully watched → white checkmark badge (top-right)
|
// TV: Show white checkmark when watched
|
||||||
if (isPlayed && (isMovieOrEpisode || isAggregate)) {
|
if (
|
||||||
|
item.UserData?.Played &&
|
||||||
|
(item.Type === "Movie" || item.Type === "Episode")
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<View style={[tvBadgeBase, { width: scaleSize(28) }]}>
|
<View
|
||||||
<Ionicons name='checkmark' size={scaleSize(18)} color='black' />
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.9)",
|
||||||
|
borderRadius: 14,
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name='checkmark' size={18} color='black' />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Series/BoxSet with remaining episodes → count badge
|
return null;
|
||||||
return <UnplayedCountBadge item={item} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mobile: purple corner ribbon for unwatched Movie/Episode (existing behavior)
|
// Mobile: Show purple triangle for unwatched
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isMovieOrEpisode && !isPlayed && (
|
{item.UserData?.Played === false &&
|
||||||
<View className='bg-purple-600 w-8 h-8 absolute -top-4 -right-4 rotate-45' />
|
(item.Type === "Movie" || item.Type === "Episode") && (
|
||||||
)}
|
<View className='bg-purple-600 w-8 h-8 absolute -top-4 -right-4 rotate-45' />
|
||||||
|
)}
|
||||||
{/* Fully watched Series/BoxSet → small purple checkmark */}
|
|
||||||
{isAggregate && isPlayed && (
|
|
||||||
<View style={[mobileBadgeBase, { width: 20 }]}>
|
|
||||||
<Ionicons name='checkmark' size={13} color='white' />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Series/BoxSet with remaining episodes → count badge */}
|
|
||||||
<UnplayedCountBadge item={item} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { type BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { View, type ViewProps } from "react-native";
|
import { View, type ViewProps } from "react-native";
|
||||||
import { WatchedIndicator } from "@/components/WatchedIndicator";
|
|
||||||
import { ItemImage } from "../common/ItemImage";
|
import { ItemImage } from "../common/ItemImage";
|
||||||
|
import { WatchedIndicator } from "../WatchedIndicator";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Image } from "expo-image";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { WatchedIndicator } from "@/components/WatchedIndicator";
|
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||||
|
|
||||||
@@ -53,7 +52,6 @@ const SeriesPoster: React.FC<MoviePosterProps> = ({ item }) => {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<WatchedIndicator item={item} />
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { ProgressBar } from "@/components/common/ProgressBar";
|
import { ProgressBar } from "@/components/common/ProgressBar";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import {
|
import { WatchedIndicator } from "@/components/WatchedIndicator";
|
||||||
UnplayedCountBadge,
|
|
||||||
WatchedIndicator,
|
|
||||||
} from "@/components/WatchedIndicator";
|
|
||||||
import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
|
import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
|
||||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||||
import {
|
import {
|
||||||
@@ -430,12 +427,6 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
/>
|
/>
|
||||||
{PlayButtonOverlay}
|
{PlayButtonOverlay}
|
||||||
{NowPlayingBadge}
|
{NowPlayingBadge}
|
||||||
{/*
|
|
||||||
The glass view draws the watched checkmark natively but cannot show
|
|
||||||
an unplayed-episode count, so render it as an RN overlay on top.
|
|
||||||
Returns null when not applicable (non-series / fully watched).
|
|
||||||
*/}
|
|
||||||
<UnplayedCountBadge item={item} />
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user