mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-16 09:23:07 +01:00
docs: update README and fix typos across codebase
Enhances README with comprehensive feature categorization including Media Playback, Media Management, and Advanced Features sections Expands documentation for music library support, search providers (Marlin, Streamystats, Jellysearch), and plugin functionality Updates FAQ section with more detailed answers about library visibility, downloads, subtitles, TV platform support, and Seerr integration Corrects typos throughout the application: - Fixes "liraries" to "libraries" in hide libraries settings - Changes "centralised" to "centralized" for consistency - Updates "Jellyseerr" references to "Seerr" throughout codebase Adds missing translations for player settings including aspect ratio options, alignment controls, and MPV subtitle customization Improves consistency in capitalization and punctuation across translation strings
This commit is contained in:
73
README.md
73
README.md
@@ -22,13 +22,28 @@
|
|||||||
|
|
||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
- 🚀 **Skip Intro / Credits Support**: Lets you quickly skip intros and credits during playback
|
### 🎬 Media Playback
|
||||||
- 🖼️ **Trickplay images**: The new golden standard for chapter previews when seeking
|
- 🚀 **Skip Intro / Credits**: Automatically skip intros and credits during playback
|
||||||
- 📥 **Download media**: Save your media locally and watch it offline
|
- 🖼️ **Trickplay Images**: Chapter previews with thumbnails when seeking
|
||||||
- ⚙️ **Settings management**: Manage app configurations for all users through our plugin
|
- 🎵 **Music Library**: Full support for music playback with playlists and queue management
|
||||||
- 🤖 **Seerr (formerly Jellyseerr) integration**: Request media directly in the app
|
- 📺 **Live TV**: Watch and record live television streams
|
||||||
- 👁️ **Sessions view:** View all active sessions currently streaming on your server
|
|
||||||
- 📡 **Chromecast**: Cast your media to any Chromecast-enabled device
|
- 📡 **Chromecast**: Cast your media to any Chromecast-enabled device
|
||||||
|
- 🎥 **MPV Player**: Powerful open-source player with wide format support
|
||||||
|
|
||||||
|
### 📱 Media Management
|
||||||
|
- 📥 **Download Media**: Save movies, shows, and music locally for offline viewing
|
||||||
|
- ⭐ **Favorites**: Quick access to your favorite content
|
||||||
|
- 📋 **Watchlists**: Create and manage custom watchlists with Streamystats integration
|
||||||
|
- 🔖 **Continue Watching**: Pick up right where you left off
|
||||||
|
- 🎯 **Next Up**: Smart suggestions for your next episode
|
||||||
|
|
||||||
|
### ⚙️ Advanced Features
|
||||||
|
- 🤖 **Seerr Integration**: Request new media directly in the app
|
||||||
|
- 🔍 **Smart Search**: Powerful search with Marlin Search and Streamystats support
|
||||||
|
- 👁️ **Active Sessions**: View all active streams on your server
|
||||||
|
- 🌐 **Multi-Language**: Available in 20+ languages with Crowdin integration
|
||||||
|
- 🎨 **Customizable**: Personalize your home screen and settings
|
||||||
|
- 🔌 **Plugin System**: Centralized settings sync across all devices via Jellyfin plugin
|
||||||
|
|
||||||
## 🧪 Experimental Features
|
## 🧪 Experimental Features
|
||||||
|
|
||||||
@@ -40,12 +55,13 @@ Downloading works by using FFmpeg to convert an HLS stream into a video file on
|
|||||||
|
|
||||||
### 🧩 Streamyfin Plugin
|
### 🧩 Streamyfin Plugin
|
||||||
|
|
||||||
The Jellyfin Plugin for Streamyfin is a plugin you install into Jellyfin that holds all settings for the client Streamyfin. This allows you to synchronize settings across all your users, like for example:
|
The Jellyfin Plugin for Streamyfin synchronizes settings across all your devices and users. Install it on your Jellyfin server to enable:
|
||||||
|
|
||||||
- Automatic Seerr login with no user input required
|
- Automatic Seerr login with no user input required
|
||||||
- Set your preferred default languages
|
- Default language preferences for audio and subtitles
|
||||||
- Configure download method and search provider
|
- Configure download settings and search providers (Marlin, Streamystats)
|
||||||
- Personalize your home screen
|
- Customize your home screen layout and sections
|
||||||
|
- Centralized configuration management
|
||||||
- And much more
|
- And much more
|
||||||
|
|
||||||
[Streamyfin Plugin](https://github.com/streamyfin/jellyfin-plugin-streamyfin)
|
[Streamyfin Plugin](https://github.com/streamyfin/jellyfin-plugin-streamyfin)
|
||||||
@@ -59,11 +75,24 @@ Chromecast support is currently under development. Video casting is already avai
|
|||||||
Streamyfin uses [MPV](https://mpv.io/) as its primary video player on all platforms, powered by [MPVKit](https://github.com/mpvkit/MPVKit). MPV is a powerful, open-source media player known for its wide format support and high-quality playback.
|
Streamyfin uses [MPV](https://mpv.io/) as its primary video player on all platforms, powered by [MPVKit](https://github.com/mpvkit/MPVKit). MPV is a powerful, open-source media player known for its wide format support and high-quality playback.
|
||||||
Thanks to [@Alexk2309](https://github.com/Alexk2309) for the hard work building the native MPV module in Streamyfin.
|
Thanks to [@Alexk2309](https://github.com/Alexk2309) for the hard work building the native MPV module in Streamyfin.
|
||||||
|
|
||||||
### 🔍 Jellysearch
|
### 🎵 Music Library
|
||||||
|
|
||||||
[Jellysearch](https://gitlab.com/DomiStyle/jellysearch) works with Streamyfin
|
Streamyfin now includes full music library support with features like:
|
||||||
|
|
||||||
> A fast full-text search proxy for Jellyfin. Integrates seamlessly with most Jellyfin clients.
|
- Browse your music collection by artists, albums, and playlists
|
||||||
|
- Create and manage custom playlists
|
||||||
|
- Queue management with shuffle and repeat
|
||||||
|
- Background playback
|
||||||
|
- Now playing screen with album artwork
|
||||||
|
- Download songs for offline listening
|
||||||
|
|
||||||
|
### 🔍 Advanced Search
|
||||||
|
|
||||||
|
Streamyfin supports multiple search providers:
|
||||||
|
|
||||||
|
- **Marlin Search**: Fast semantic search for your Jellyfin library
|
||||||
|
- **Streamystats**: Advanced statistics and personalized recommendations
|
||||||
|
- **Jellysearch**: Fast full-text search proxy ([Jellysearch](https://gitlab.com/DomiStyle/jellysearch))
|
||||||
|
|
||||||
## 🛣️ Roadmap
|
## 🛣️ Roadmap
|
||||||
|
|
||||||
@@ -131,10 +160,20 @@ Need assistance or have any questions?
|
|||||||
|
|
||||||
## ❓ FAQ
|
## ❓ FAQ
|
||||||
|
|
||||||
1. Q: Why can't I see my libraries in Streamyfin?
|
1. **Q: Why can't I see my libraries in Streamyfin?**
|
||||||
A: Make sure your server is running one of the latest versions and that you have at least one library that isn't audio only
|
A: Ensure your Jellyfin server is running a recent version (10.10.0+) and that you have proper permissions to access the libraries.
|
||||||
2. Q: Why can't I see my music library?
|
|
||||||
A: We don't currently support music and are unlikely to support music in the near future
|
2. **Q: How do I enable downloads?**
|
||||||
|
A: Downloads use FFmpeg to convert HLS streams. Ensure your server has transcoding enabled and sufficient resources.
|
||||||
|
|
||||||
|
3. **Q: Does Streamyfin support subtitles?**
|
||||||
|
A: Yes, with full customization including size, color, position, and automatic language selection.
|
||||||
|
|
||||||
|
4. **Q: Can I use Streamyfin on Apple TV or Android TV?**
|
||||||
|
A: Yes, Streamyfin has dedicated TV builds optimized for remote control navigation. Please note that TV platforms are currently in early development and not very stable. Android TV is currently the most reliable platform for testing.
|
||||||
|
|
||||||
|
5. **Q: How do I set up Seerr integration?**
|
||||||
|
A: Go to Settings → Plugins → Seerr, enter your server URL and Jellyfin credentials.
|
||||||
|
|
||||||
## 📝 Credits
|
## 📝 Credits
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default function page() {
|
|||||||
))}
|
))}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
||||||
{t("home.settings.other.select_liraries_you_want_to_hide")}
|
{t("home.settings.other.select_libraries_you_want_to_hide")}
|
||||||
</Text>
|
</Text>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export default function page() {
|
|||||||
))}
|
))}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
<Text className='px-4 text-xs text-neutral-500 mt-1'>
|
||||||
{t("home.settings.other.select_liraries_you_want_to_hide")}
|
{t("home.settings.other.select_libraries_you_want_to_hide")}
|
||||||
</Text>
|
</Text>
|
||||||
</DisabledSetting>
|
</DisabledSetting>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -158,12 +158,12 @@ export const IntroSheet = forwardRef<IntroSheetRef>((_, ref) => {
|
|||||||
</View>
|
</View>
|
||||||
<View className='shrink ml-2'>
|
<View className='shrink ml-2'>
|
||||||
<Text className='font-bold mb-1'>
|
<Text className='font-bold mb-1'>
|
||||||
{t("home.intro.centralised_settings_plugin_title")}
|
{t("home.intro.centralized_settings_plugin_title")}
|
||||||
</Text>
|
</Text>
|
||||||
<View className='flex-row flex-wrap items-baseline'>
|
<View className='flex-row flex-wrap items-baseline'>
|
||||||
<Text className='shrink text-xs'>
|
<Text className='shrink text-xs'>
|
||||||
{t(
|
{t(
|
||||||
"home.intro.centralised_settings_plugin_description",
|
"home.intro.centralized_settings_plugin_description",
|
||||||
)}{" "}
|
)}{" "}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export const LibraryOptionsSheet: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</OptionGroup>
|
</OptionGroup>
|
||||||
|
|
||||||
<OptionGroup title='Options'>
|
<OptionGroup title={t("library.options.options_title")}>
|
||||||
<ToggleItem
|
<ToggleItem
|
||||||
label={t("library.options.show_titles")}
|
label={t("library.options.show_titles")}
|
||||||
value={settings.showTitles}
|
value={settings.showTitles}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, View, type ViewProps } from "react-native";
|
import { Platform, View, type ViewProps } from "react-native";
|
||||||
import { Stepper } from "@/components/inputs/Stepper";
|
import { Stepper } from "@/components/inputs/Stepper";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
@@ -17,20 +18,21 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
const isTv = Platform.isTV;
|
const isTv = Platform.isTV;
|
||||||
const media = useMedia();
|
const media = useMedia();
|
||||||
const { settings, updateSettings } = media;
|
const { settings, updateSettings } = media;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const alignXOptions: AlignX[] = ["left", "center", "right"];
|
const alignXOptions: AlignX[] = ["left", "center", "right"];
|
||||||
const alignYOptions: AlignY[] = ["top", "center", "bottom"];
|
const alignYOptions: AlignY[] = ["top", "center", "bottom"];
|
||||||
|
|
||||||
const alignXLabels: Record<AlignX, string> = {
|
const alignXLabels: Record<AlignX, string> = {
|
||||||
left: "Left",
|
left: t("player.alignment_left"),
|
||||||
center: "Center",
|
center: t("player.alignment_center"),
|
||||||
right: "Right",
|
right: t("player.alignment_right"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const alignYLabels: Record<AlignY, string> = {
|
const alignYLabels: Record<AlignY, string> = {
|
||||||
top: "Top",
|
top: t("player.alignment_top"),
|
||||||
center: "Center",
|
center: t("player.alignment_center"),
|
||||||
bottom: "Bottom",
|
bottom: t("player.alignment_bottom"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const alignXOptionGroups = useMemo(() => {
|
const alignXOptionGroups = useMemo(() => {
|
||||||
@@ -61,14 +63,14 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup
|
<ListGroup
|
||||||
title='MPV Subtitle Settings'
|
title={t("player.mpv_subtitle_settings_title")}
|
||||||
description={
|
description={
|
||||||
<Text className='text-[#8E8D91] text-xs'>
|
<Text className='text-[#8E8D91] text-xs'>
|
||||||
Advanced subtitle customization for MPV player
|
{t("player.mpv_subtitle_settings_description")}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem title='Subtitle Scale'>
|
<ListItem title={t("player.subtitle_scale")}>
|
||||||
<Stepper
|
<Stepper
|
||||||
value={settings.mpvSubtitleScale ?? 1.0}
|
value={settings.mpvSubtitleScale ?? 1.0}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
@@ -80,7 +82,7 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem title='Vertical Margin'>
|
<ListItem title={t("player.vertical_margin")}>
|
||||||
<Stepper
|
<Stepper
|
||||||
value={settings.mpvSubtitleMarginY ?? 0}
|
value={settings.mpvSubtitleMarginY ?? 0}
|
||||||
step={5}
|
step={5}
|
||||||
@@ -90,7 +92,7 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem title='Horizontal Alignment'>
|
<ListItem title={t("player.horizontal_alignment")}>
|
||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={alignXOptionGroups}
|
groups={alignXOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
@@ -105,11 +107,11 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
title='Horizontal Alignment'
|
title={t("player.horizontal_alignment")}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem title='Vertical Alignment'>
|
<ListItem title={t("player.vertical_alignment")}>
|
||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
groups={alignYOptionGroups}
|
groups={alignYOptionGroups}
|
||||||
trigger={
|
trigger={
|
||||||
@@ -124,7 +126,7 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
title='Vertical Alignment'
|
title={t("player.vertical_alignment")}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|||||||
@@ -20,22 +20,22 @@ export const PluginSettings = () => {
|
|||||||
>
|
>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => router.push("/settings/plugins/jellyseerr/page")}
|
onPress={() => router.push("/settings/plugins/jellyseerr/page")}
|
||||||
title={"Jellyseerr"}
|
title={"Seerr"}
|
||||||
showArrow
|
|
||||||
/>
|
|
||||||
<ListItem
|
|
||||||
onPress={() => router.push("/settings/plugins/marlin-search/page")}
|
|
||||||
title='Marlin Search'
|
|
||||||
showArrow
|
showArrow
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => router.push("/settings/plugins/streamystats/page")}
|
onPress={() => router.push("/settings/plugins/streamystats/page")}
|
||||||
title='Streamystats'
|
title={"Streamystats"}
|
||||||
|
showArrow
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
onPress={() => router.push("/settings/plugins/marlin-search/page")}
|
||||||
|
title={"Marlin Search"}
|
||||||
showArrow
|
showArrow
|
||||||
/>
|
/>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={() => router.push("/settings/plugins/kefinTweaks/page")}
|
onPress={() => router.push("/settings/plugins/kefinTweaks/page")}
|
||||||
title='KefinTweaks'
|
title={"KefinTweaks"}
|
||||||
showArrow
|
showArrow
|
||||||
/>
|
/>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import {
|
import {
|
||||||
type OptionGroup,
|
type OptionGroup,
|
||||||
@@ -18,49 +19,50 @@ interface AspectRatioSelectorProps {
|
|||||||
interface AspectRatioOption {
|
interface AspectRatioOption {
|
||||||
id: AspectRatio;
|
id: AspectRatio;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
descriptionKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ASPECT_RATIO_OPTIONS: AspectRatioOption[] = [
|
|
||||||
{
|
|
||||||
id: "default",
|
|
||||||
label: "Original",
|
|
||||||
description: "Use video's original aspect ratio",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "16:9",
|
|
||||||
label: "16:9",
|
|
||||||
description: "Widescreen (most common)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4:3",
|
|
||||||
label: "4:3",
|
|
||||||
description: "Traditional TV format",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "1:1",
|
|
||||||
label: "1:1",
|
|
||||||
description: "Square format",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "21:9",
|
|
||||||
label: "21:9",
|
|
||||||
description: "Ultra-wide cinematic",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
||||||
currentRatio,
|
currentRatio,
|
||||||
onRatioChange,
|
onRatioChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const lightHapticFeedback = useHaptic("light");
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleRatioSelect = (ratio: AspectRatio) => {
|
const handleRatioSelect = (ratio: AspectRatio) => {
|
||||||
onRatioChange(ratio);
|
onRatioChange(ratio);
|
||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ASPECT_RATIO_OPTIONS: AspectRatioOption[] = [
|
||||||
|
{
|
||||||
|
id: "default",
|
||||||
|
label: t("player.aspect_ratio_original"),
|
||||||
|
descriptionKey: "player.aspect_ratio_original_description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "16:9",
|
||||||
|
label: "16:9",
|
||||||
|
descriptionKey: "player.aspect_ratio_16_9_description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4:3",
|
||||||
|
label: "4:3",
|
||||||
|
descriptionKey: "player.aspect_ratio_4_3_description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "1:1",
|
||||||
|
label: "1:1",
|
||||||
|
descriptionKey: "player.aspect_ratio_1_1_description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "21:9",
|
||||||
|
label: "21:9",
|
||||||
|
descriptionKey: "player.aspect_ratio_21_9_description",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const optionGroups = useMemo<OptionGroup[]>(() => {
|
const optionGroups = useMemo<OptionGroup[]>(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -94,7 +96,7 @@ export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
title='Aspect Ratio'
|
title={t("player.aspect_ratio_title")}
|
||||||
groups={optionGroups}
|
groups={optionGroups}
|
||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
bottomSheetConfig={{
|
bottomSheetConfig={{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { useCallback, useMemo, useRef } from "react";
|
import { useCallback, useMemo, useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform, View } from "react-native";
|
import { Platform, View } from "react-native";
|
||||||
import { BITRATES } from "@/components/BitrateSelector";
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
import {
|
import {
|
||||||
@@ -41,6 +42,7 @@ const DropdownView = ({
|
|||||||
const { settings, updateSettings } = useSettings();
|
const { settings, updateSettings } = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isOffline = useOfflineMode();
|
const isOffline = useOfflineMode();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { subtitleIndex, audioIndex, bitrateValue, playbackPosition } =
|
const { subtitleIndex, audioIndex, bitrateValue, playbackPosition } =
|
||||||
useLocalSearchParams<{
|
useLocalSearchParams<{
|
||||||
@@ -194,7 +196,7 @@ const DropdownView = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PlatformDropdown
|
<PlatformDropdown
|
||||||
title='Playback Options'
|
title={t("player.playback_options_title")}
|
||||||
groups={optionGroups}
|
groups={optionGroups}
|
||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
expoUIConfig={{}}
|
expoUIConfig={{}}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export class JellyseerrApi {
|
|||||||
}
|
}
|
||||||
toast.error(t("jellyseerr.toasts.jellyseerr_test_failed"));
|
toast.error(t("jellyseerr.toasts.jellyseerr_test_failed"));
|
||||||
writeErrorLog(
|
writeErrorLog(
|
||||||
`Jellyseerr returned a ${status} for url:\n${response.config.url}`,
|
`Seerr returned a ${status} for url:\n${response.config.url}`,
|
||||||
response.data,
|
response.data,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -400,7 +400,7 @@ export class JellyseerrApi {
|
|||||||
},
|
},
|
||||||
(error: AxiosError) => {
|
(error: AxiosError) => {
|
||||||
writeErrorLog(
|
writeErrorLog(
|
||||||
`Jellyseerr response error\nerror: ${error.toString()}\nurl: ${error?.config?.url}`,
|
`Seerr response error\nerror: ${error.toString()}\nurl: ${error?.config?.url}`,
|
||||||
error.response?.data,
|
error.response?.data,
|
||||||
);
|
);
|
||||||
if (error.response?.status === 403) {
|
if (error.response?.status === 403) {
|
||||||
@@ -425,7 +425,7 @@ export class JellyseerrApi {
|
|||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error("Jellyseerr request error", error);
|
console.error("Seerr request error", error);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { MpvPlayerViewProps } from "./MpvPlayer.types";
|
import { MpvPlayerViewProps } from "./MpvPlayer.types";
|
||||||
|
|
||||||
export default function MpvPlayerView(props: MpvPlayerViewProps) {
|
export default function MpvPlayerView(props: MpvPlayerViewProps) {
|
||||||
const url = props.source?.url ?? "";
|
const url = props.source?.url ?? "";
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<iframe
|
<iframe
|
||||||
title='MPV Player'
|
title={t("player.mpv_player_title")}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
src={url}
|
src={url}
|
||||||
onLoad={() => props.onLoad?.({ nativeEvent: { url } })}
|
onLoad={() => props.onLoad?.({ nativeEvent: { url } })}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ function runTypeCheck() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log(
|
log(
|
||||||
`✅ ${colors.bold}TypeScript check passed${colors.reset} ${colors.gray}(jellyseerr utils errors ignored)${colors.reset}`,
|
`✅ ${colors.bold}TypeScript check passed${colors.reset} ${colors.gray}(seerr utils errors ignored)${colors.reset}`,
|
||||||
colors.green,
|
colors.green,
|
||||||
);
|
);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
"login": {
|
"login": {
|
||||||
"username_required": "Username is required",
|
"username_required": "Username is required",
|
||||||
"error_title": "Error",
|
"error_title": "Error",
|
||||||
"login_title": "Log In",
|
"login_title": "Log in",
|
||||||
"login_to_title": "Log in to",
|
"login_to_title": "Log in to",
|
||||||
"username_placeholder": "Username",
|
"username_placeholder": "Username",
|
||||||
"password_placeholder": "Password",
|
"password_placeholder": "Password",
|
||||||
"login_button": "Log In",
|
"login_button": "Log in",
|
||||||
"quick_connect": "Quick Connect",
|
"quick_connect": "Quick Connect",
|
||||||
"enter_code_to_login": "Enter code {{code}} to log in",
|
"enter_code_to_login": "Enter code {{code}} to log in",
|
||||||
"failed_to_initiate_quick_connect": "Failed to initiate Quick Connect",
|
"failed_to_initiate_quick_connect": "Failed to initiate Quick Connect",
|
||||||
"got_it": "Got It",
|
"got_it": "Got It",
|
||||||
"connection_failed": "Connection Failed",
|
"connection_failed": "Connection Failed",
|
||||||
"could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
|
"could_not_connect_to_server": "Could not connect to the server. Please check the URL and your network connection.",
|
||||||
"an_unexpected_error_occured": "An Unexpected Error Occurred",
|
"an_unexpected_error_occured": "An unexpected error occurred",
|
||||||
"change_server": "Change Server",
|
"change_server": "Change Server",
|
||||||
"invalid_username_or_password": "Invalid Username or Password",
|
"invalid_username_or_password": "Invalid Username or Password",
|
||||||
"user_does_not_have_permission_to_log_in": "User does not have permission to log in",
|
"user_does_not_have_permission_to_log_in": "User does not have permission to log in",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"there_is_a_server_error": "There is a server error",
|
"there_is_a_server_error": "There is a server error",
|
||||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
|
"an_unexpected_error_occured_did_you_enter_the_correct_url": "An unexpected error occurred. Did you enter the server URL correctly?",
|
||||||
"too_old_server_text": "Unsupported Jellyfin Server Discovered",
|
"too_old_server_text": "Unsupported Jellyfin Server Discovered",
|
||||||
"too_old_server_description": "Please update Jellyfin to the latest version"
|
"too_old_server_description": "Please update Jellyfin to the latest version."
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",
|
"enter_url_to_jellyfin_server": "Enter the URL to your Jellyfin server",
|
||||||
@@ -93,22 +93,22 @@
|
|||||||
"suggested_episodes": "Suggested Episodes",
|
"suggested_episodes": "Suggested Episodes",
|
||||||
"intro": {
|
"intro": {
|
||||||
"welcome_to_streamyfin": "Welcome to Streamyfin",
|
"welcome_to_streamyfin": "Welcome to Streamyfin",
|
||||||
"a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin.",
|
"a_free_and_open_source_client_for_jellyfin": "A Free and Open-Source Client for Jellyfin",
|
||||||
"features_title": "Features",
|
"features_title": "Features",
|
||||||
"features_description": "Streamyfin has a bunch of features and integrates with a wide array of software which you can find in the settings menu; these include:",
|
"features_description": "Streamyfin offers many features and integrates with a wide array of software which you can find in the settings menu, including:",
|
||||||
"jellyseerr_feature_description": "Connect to your Seerr instance and request movies directly in the app.",
|
"jellyseerr_feature_description": "Connect to your Seerr instance and request movies directly in the app.",
|
||||||
"downloads_feature_title": "Downloads",
|
"downloads_feature_title": "Downloads",
|
||||||
"downloads_feature_description": "Download movies and tv shows to view offline.",
|
"downloads_feature_description": "Download movies and TV shows to view offline.",
|
||||||
"chromecast_feature_description": "Cast movies and tv shows to your Chromecast devices.",
|
"chromecast_feature_description": "Cast movies and TV shows to your Chromecast devices.",
|
||||||
"centralised_settings_plugin_title": "Centralised Settings Plugin",
|
"centralized_settings_plugin_title": "Centralized Settings Plugin",
|
||||||
"centralised_settings_plugin_description": "Configure settings from a centralised location on your Jellyfin server. All client settings for all users will be synced automatically.",
|
"centralized_settings_plugin_description": "Configure settings from a centralized location on your Jellyfin server. All client settings for all users will be synced automatically.",
|
||||||
"done_button": "Done",
|
"done_button": "Done",
|
||||||
"go_to_settings_button": "Go to Settings",
|
"go_to_settings_button": "Go to Settings",
|
||||||
"read_more": "Read More"
|
"read_more": "Read More"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"settings_title": "Settings",
|
"settings_title": "Settings",
|
||||||
"log_out_button": "Log Out",
|
"log_out_button": "Log out",
|
||||||
"categories": {
|
"categories": {
|
||||||
"title": "Categories"
|
"title": "Categories"
|
||||||
},
|
},
|
||||||
@@ -302,12 +302,12 @@
|
|||||||
"show_custom_menu_links": "Show Custom Menu Links",
|
"show_custom_menu_links": "Show Custom Menu Links",
|
||||||
"show_large_home_carousel": "Show Large Home Carousel (beta)",
|
"show_large_home_carousel": "Show Large Home Carousel (beta)",
|
||||||
"hide_libraries": "Hide Libraries",
|
"hide_libraries": "Hide Libraries",
|
||||||
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
"select_libraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
||||||
"disable_haptic_feedback": "Disable Haptic Feedback",
|
"disable_haptic_feedback": "Disable Haptic Feedback",
|
||||||
"default_quality": "Default Quality",
|
"default_quality": "Default Quality",
|
||||||
"default_playback_speed": "Default Playback Speed",
|
"default_playback_speed": "Default Playback Speed",
|
||||||
"auto_play_next_episode": "Auto-play Next Episode",
|
"auto_play_next_episode": "Autoplay Next Episode",
|
||||||
"max_auto_play_episode_count": "Max Auto Play Episode Count",
|
"max_auto_play_episode_count": "Max Autoplay Episode Count",
|
||||||
"disabled": "Disabled"
|
"disabled": "Disabled"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
@@ -327,7 +327,7 @@
|
|||||||
"plugins": {
|
"plugins": {
|
||||||
"plugins_title": "Plugins",
|
"plugins_title": "Plugins",
|
||||||
"jellyseerr": {
|
"jellyseerr": {
|
||||||
"jellyseerr_warning": "This integration is in its early stages. Expect things to change.",
|
"jellyseerr_warning": "This integration is in early development. Features may change.",
|
||||||
"server_url": "Server URL",
|
"server_url": "Server URL",
|
||||||
"server_url_hint": "Example: http(s)://your-host.url\n(add port if required)",
|
"server_url_hint": "Example: http(s)://your-host.url\n(add port if required)",
|
||||||
"server_url_placeholder": "Seerr URL",
|
"server_url_placeholder": "Seerr URL",
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
"tv_quota_days": "TV Quota Days",
|
"tv_quota_days": "TV Quota Days",
|
||||||
"reset_jellyseerr_config_button": "Reset Seerr Config",
|
"reset_jellyseerr_config_button": "Reset Seerr Config",
|
||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
"plus_n_more": "+{{n}} More",
|
"plus_n_more": "+{{n}} more",
|
||||||
"order_by": {
|
"order_by": {
|
||||||
"DEFAULT": "Default",
|
"DEFAULT": "Default",
|
||||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
||||||
@@ -353,7 +353,7 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"server_url_placeholder": "http(s)://domain.org:port",
|
"server_url_placeholder": "http(s)://domain.org:port",
|
||||||
"marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.",
|
"marlin_search_hint": "Enter the URL for the Marlin server. The URL should include http or https and optionally the port.",
|
||||||
"read_more_about_marlin": "Read More About Marlin.",
|
"read_more_about_marlin": "Read more about Marlin.",
|
||||||
"save_button": "Save",
|
"save_button": "Save",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"saved": "Saved",
|
"saved": "Saved",
|
||||||
@@ -368,7 +368,7 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"server_url_placeholder": "http(s)://streamystats.example.com",
|
"server_url_placeholder": "http(s)://streamystats.example.com",
|
||||||
"streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.",
|
"streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.",
|
||||||
"read_more_about_streamystats": "Read More About Streamystats.",
|
"read_more_about_streamystats": "Read more about Streamystats.",
|
||||||
"save_button": "Save",
|
"save_button": "Save",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"features_title": "Features",
|
"features_title": "Features",
|
||||||
@@ -427,18 +427,18 @@
|
|||||||
"system": "System"
|
"system": "System"
|
||||||
},
|
},
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"error_deleting_files": "Error Deleting Files",
|
"error_deleting_files": "Error deleting files",
|
||||||
"background_downloads_enabled": "Background downloads enabled",
|
"background_downloads_enabled": "Background downloads enabled",
|
||||||
"background_downloads_disabled": "Background downloads disabled"
|
"background_downloads_disabled": "Background downloads disabled"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
"title": "Sessions",
|
"title": "Sessions",
|
||||||
"no_active_sessions": "No Active Sessions"
|
"no_active_sessions": "No active sessions"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
"tvseries": "TV-Series",
|
"tvseries": "TV Series",
|
||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
"other_media": "Other media",
|
"other_media": "Other media",
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
"no_items_in_queue": "No Items in Queue",
|
"no_items_in_queue": "No Items in Queue",
|
||||||
"no_downloaded_items": "No Downloaded Items",
|
"no_downloaded_items": "No Downloaded Items",
|
||||||
"delete_all_movies_button": "Delete All Movies",
|
"delete_all_movies_button": "Delete All Movies",
|
||||||
"delete_all_tvseries_button": "Delete All TV-Series",
|
"delete_all_tvseries_button": "Delete All TV Series",
|
||||||
"delete_all_button": "Delete All",
|
"delete_all_button": "Delete All",
|
||||||
"delete_all_other_media_button": "Delete other media",
|
"delete_all_other_media_button": "Delete other media",
|
||||||
"active_download": "Active Download",
|
"active_download": "Active Download",
|
||||||
@@ -461,27 +461,27 @@
|
|||||||
"eta": "ETA {{eta}}",
|
"eta": "ETA {{eta}}",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
||||||
"deleted_all_movies_successfully": "Deleted All Movies Successfully!",
|
"deleted_all_movies_successfully": "Deleted all movies successfully!",
|
||||||
"failed_to_delete_all_movies": "Failed to Delete All Movies",
|
"failed_to_delete_all_movies": "Failed to delete all movies",
|
||||||
"deleted_all_tvseries_successfully": "Deleted All TV-Series Successfully!",
|
"deleted_all_tvseries_successfully": "Deleted all TV series successfully!",
|
||||||
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
|
"failed_to_delete_all_tvseries": "Failed to delete all TV series",
|
||||||
"deleted_media_successfully": "Deleted other media Successfully!",
|
"deleted_media_successfully": "Deleted other media successfully!",
|
||||||
"failed_to_delete_media": "Failed to Delete other media",
|
"failed_to_delete_media": "Failed to delete other media",
|
||||||
"download_deleted": "Download Deleted",
|
"download_deleted": "Download deleted",
|
||||||
"download_cancelled": "Download Cancelled",
|
"download_cancelled": "Download cancelled",
|
||||||
"could_not_delete_download": "Could Not Delete Download",
|
"could_not_delete_download": "Could not delete download",
|
||||||
"download_paused": "Download Paused",
|
"download_paused": "Download paused",
|
||||||
"could_not_pause_download": "Could Not Pause Download",
|
"could_not_pause_download": "Could not pause download",
|
||||||
"download_resumed": "Download Resumed",
|
"download_resumed": "Download resumed",
|
||||||
"could_not_resume_download": "Could Not Resume Download",
|
"could_not_resume_download": "Could not resume download",
|
||||||
"download_completed": "Download Completed",
|
"download_completed": "Download completed",
|
||||||
"download_failed": "Download Failed",
|
"download_failed": "Download failed",
|
||||||
"download_failed_for_item": "Download failed for {{item}} - {{error}}",
|
"download_failed_for_item": "Download failed for {{item}} - {{error}}",
|
||||||
"download_completed_for_item": "Download Completed for {{item}}",
|
"download_completed_for_item": "Download completed for {{item}}",
|
||||||
"download_started_for_item": "Download Started for {{item}}",
|
"download_started_for_item": "Download started for {{item}}",
|
||||||
"failed_to_start_download": "Failed to start download",
|
"failed_to_start_download": "Failed to start download",
|
||||||
"item_already_downloading": "{{item}} is already downloading",
|
"item_already_downloading": "{{item}} is already downloading",
|
||||||
"all_files_deleted": "All Downloads Deleted Successfully",
|
"all_files_deleted": "All downloads deleted successfully",
|
||||||
"files_deleted_by_type": "{{count}} {{type}} deleted",
|
"files_deleted_by_type": "{{count}} {{type}} deleted",
|
||||||
"all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully",
|
"all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully",
|
||||||
"failed_to_clean_cache_directory": "Failed to clean cache directory",
|
"failed_to_clean_cache_directory": "Failed to clean cache directory",
|
||||||
@@ -493,7 +493,7 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"no_trailer_available": "No trailer available",
|
"no_trailer_available": "No Trailer Available",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitle": "Subtitle",
|
"subtitle": "Subtitle",
|
||||||
@@ -515,7 +515,7 @@
|
|||||||
"library": "Library",
|
"library": "Library",
|
||||||
"discover": "Discover",
|
"discover": "Discover",
|
||||||
"no_results": "No Results",
|
"no_results": "No Results",
|
||||||
"no_results_found_for": "No Results Found For",
|
"no_results_found_for": "No results found for",
|
||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
"series": "Series",
|
"series": "Series",
|
||||||
"episodes": "Episodes",
|
"episodes": "Episodes",
|
||||||
@@ -559,6 +559,7 @@
|
|||||||
"items": "Items"
|
"items": "Items"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
"options_title": "Options",
|
||||||
"display": "Display",
|
"display": "Display",
|
||||||
"row": "Row",
|
"row": "Row",
|
||||||
"list": "List",
|
"list": "List",
|
||||||
@@ -608,7 +609,27 @@
|
|||||||
"downloaded_file_message": "Do you want to play the downloaded file?",
|
"downloaded_file_message": "Do you want to play the downloaded file?",
|
||||||
"downloaded_file_yes": "Yes",
|
"downloaded_file_yes": "Yes",
|
||||||
"downloaded_file_no": "No",
|
"downloaded_file_no": "No",
|
||||||
"downloaded_file_cancel": "Cancel"
|
"downloaded_file_cancel": "Cancel",
|
||||||
|
"aspect_ratio_title": "Aspect Ratio",
|
||||||
|
"aspect_ratio_original": "Original",
|
||||||
|
"aspect_ratio_original_description": "Use video's original aspect ratio",
|
||||||
|
"aspect_ratio_16_9_description": "Widescreen (most common)",
|
||||||
|
"aspect_ratio_4_3_description": "Traditional TV format",
|
||||||
|
"aspect_ratio_1_1_description": "Square format",
|
||||||
|
"aspect_ratio_21_9_description": "Ultra-wide cinematic",
|
||||||
|
"playback_options_title": "Playback Options",
|
||||||
|
"mpv_subtitle_settings_title": "MPV Subtitle Settings",
|
||||||
|
"mpv_subtitle_settings_description": "Advanced subtitle customization for MPV player",
|
||||||
|
"subtitle_scale": "Subtitle Scale",
|
||||||
|
"vertical_margin": "Vertical Margin",
|
||||||
|
"horizontal_alignment": "Horizontal Alignment",
|
||||||
|
"vertical_alignment": "Vertical Alignment",
|
||||||
|
"alignment_left": "Left",
|
||||||
|
"alignment_center": "Center",
|
||||||
|
"alignment_right": "Right",
|
||||||
|
"alignment_top": "Top",
|
||||||
|
"alignment_bottom": "Bottom",
|
||||||
|
"mpv_player_title": "MPV Player"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Next Up",
|
"next_up": "Next Up",
|
||||||
@@ -627,11 +648,11 @@
|
|||||||
"media_options": "Media Options",
|
"media_options": "Media Options",
|
||||||
"quality": "Quality",
|
"quality": "Quality",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"subtitles": "Subtitle",
|
"subtitles": "Subtitles",
|
||||||
"show_more": "Show More",
|
"show_more": "Show More",
|
||||||
"show_less": "Show Less",
|
"show_less": "Show Less",
|
||||||
"appeared_in": "Appeared In",
|
"appeared_in": "Appeared In",
|
||||||
"could_not_load_item": "Could Not Load Item",
|
"could_not_load_item": "Could not load item",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"download": {
|
"download": {
|
||||||
"download_season": "Download Season",
|
"download_season": "Download Season",
|
||||||
@@ -725,7 +746,7 @@
|
|||||||
"albums": "Albums",
|
"albums": "Albums",
|
||||||
"artists": "Artists",
|
"artists": "Artists",
|
||||||
"playlists": "Playlists",
|
"playlists": "Playlists",
|
||||||
"tracks": "tracks"
|
"tracks": "Tracks"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"all": "All"
|
"all": "All"
|
||||||
|
|||||||
Reference in New Issue
Block a user