mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 18:35:43 -04:00
chore: upgrade Expo SDK 55 → 57 and React Native 0.83 → 0.86 (#54)
This commit is contained in:
149 files changed
+3193
-4768
No files matched your search
@@ -1,48 +0,0 @@
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Skip for bot PRs and fork PRs due to OIDC token limitations in pull_request_target
|
||||
# Fork PRs fail because the actor in OIDC context is the fork contributor (read-only)
|
||||
if: |
|
||||
github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.user.type != 'Bot' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
plugin_marketplaces: "https://github.com/anthropics/claude-code.git"
|
||||
plugins: "code-review@claude-code-plugins"
|
||||
prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
track_progress: true
|
||||
claude_args: |
|
||||
--model 'claude-opus-4-6'
|
||||
--allowedTools 'mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)'
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
claude_args: "--model claude-opus-4-6"
|
||||
@@ -19,10 +19,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Upload sources to Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
uses: crowdin/github-action@v3
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
@@ -37,10 +37,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Download translations from Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
uses: crowdin/github-action@v3
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
|
||||
@@ -30,23 +30,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
${{ env.GHCR_IMAGE }}
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
type=sha,prefix=sha-
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
@@ -30,16 +30,16 @@ jobs:
|
||||
name: EAS Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
uses: expo/expo-github-action@v9
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
@@ -17,16 +17,16 @@ jobs:
|
||||
name: EAS Submit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
uses: expo/expo-github-action@v9
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
@@ -10,10 +10,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -43,6 +43,6 @@ jobs:
|
||||
run: bunx turbo run test -- --coverage
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -8,6 +8,7 @@ node_modules
|
||||
# Build outputs
|
||||
dist
|
||||
build
|
||||
.build
|
||||
*.tsbuildinfo
|
||||
.tanstack
|
||||
|
||||
|
||||
+13
-17
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["oxc", "eslint", "typescript", "react", "import", "unicorn", "vitest", "jsx-a11y"],
|
||||
"jsPlugins": ["@e18e/eslint-plugin"],
|
||||
"env": {
|
||||
"builtin": true
|
||||
},
|
||||
@@ -15,30 +14,27 @@
|
||||
"react/style-prop-object": "off",
|
||||
"import/no-unassigned-import": "off",
|
||||
"import/no-named-as-default-member": "off",
|
||||
"no-underscore-dangle": ["warn", { "allow": ["__APP_VERSION__", "__GIT_COMMIT_SHA__"] }],
|
||||
"oxc/no-barrel-file": ["warn", { "threshold": 0 }],
|
||||
"unicorn/no-array-sort": "off",
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"no-new": "off",
|
||||
"no-await-in-loop": "off",
|
||||
"react/no-array-index-key": "off",
|
||||
"react/jsx-no-useless-fragment": "error",
|
||||
"react/rules-of-hooks": "error",
|
||||
"e18e/prefer-array-at": "error",
|
||||
"e18e/prefer-array-fill": "error",
|
||||
"e18e/prefer-array-from-map": "error",
|
||||
"e18e/prefer-array-some": "error",
|
||||
"e18e/prefer-array-to-reversed": "error",
|
||||
"e18e/prefer-array-to-sorted": "error",
|
||||
"e18e/prefer-array-to-spliced": "error",
|
||||
"e18e/prefer-date-now": "error",
|
||||
"e18e/prefer-includes": "error",
|
||||
"e18e/prefer-nullish-coalescing": "error",
|
||||
"e18e/prefer-object-has-own": "error",
|
||||
"e18e/prefer-regex-test": "error",
|
||||
"e18e/prefer-spread-syntax": "off",
|
||||
"e18e/prefer-static-regex": "error",
|
||||
"e18e/prefer-timer-args": "error",
|
||||
"e18e/prefer-url-canparse": "error"
|
||||
"react/preserve-manual-memoization": "off",
|
||||
"react/capitalized-calls": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist", "build", "docs", "**/routeTree.gen.ts"]
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
"config": {
|
||||
"usesNonExemptEncryption": false
|
||||
},
|
||||
"deploymentTarget": "16.4",
|
||||
"icon": "./assets/images/sofa.icon",
|
||||
"bundleIdentifier": "com.jakejarvis.sofa",
|
||||
"appleTeamId": "B5ZWKBCUTU",
|
||||
@@ -65,14 +66,6 @@
|
||||
"package": "com.jakejarvis.sofa"
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"ios": {
|
||||
"deploymentTarget": "16.2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"expo-router",
|
||||
"expo-secure-store",
|
||||
[
|
||||
@@ -177,7 +170,9 @@
|
||||
"organization": "jarvis-0a",
|
||||
"project": "sofa-native"
|
||||
}
|
||||
]
|
||||
],
|
||||
"expo-image",
|
||||
"expo-status-bar"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true,
|
||||
|
||||
+56
-54
@@ -17,76 +17,70 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-auth/expo": "catalog:",
|
||||
"@expo/metro-runtime": "55.0.6",
|
||||
"@expo/ui": "55.0.5",
|
||||
"@formatjs/intl-datetimeformat": "7.3.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.2",
|
||||
"@formatjs/intl-locale": "5.3.1",
|
||||
"@formatjs/intl-numberformat": "9.3.1",
|
||||
"@formatjs/intl-pluralrules": "6.3.1",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.1",
|
||||
"@expo/material-symbols": "0.1.1",
|
||||
"@expo/metro-runtime": "~57.0.16",
|
||||
"@expo/ui": "~57.0.21",
|
||||
"@formatjs/intl-datetimeformat": "7.8.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.12",
|
||||
"@formatjs/intl-locale": "5.3.12",
|
||||
"@formatjs/intl-numberformat": "9.4.3",
|
||||
"@formatjs/intl-pluralrules": "6.3.15",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.15",
|
||||
"@lingui/core": "catalog:",
|
||||
"@lingui/react": "catalog:",
|
||||
"@orpc/client": "catalog:",
|
||||
"@orpc/contract": "catalog:",
|
||||
"@orpc/tanstack-query": "catalog:",
|
||||
"@react-native-menu/menu": "2.0.0",
|
||||
"@react-navigation/elements": "2.9.11",
|
||||
"@react-navigation/native": "7.1.34",
|
||||
"@sentry/react-native": "8.6.0",
|
||||
"@sentry/react-native": "~8.29.0",
|
||||
"@shopify/flash-list": "2.0.2",
|
||||
"@sofa/api": "workspace:*",
|
||||
"@sofa/i18n": "workspace:*",
|
||||
"@tabler/icons-react-native": "3.40.0",
|
||||
"@tanstack/query-async-storage-persister": "5.95.2",
|
||||
"@tabler/icons-react-native": "3.48.0",
|
||||
"@tanstack/query-async-storage-persister": "5.104.1",
|
||||
"@tanstack/react-form": "catalog:",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-query-persist-client": "5.95.2",
|
||||
"@tanstack/react-query-persist-client": "5.104.1",
|
||||
"better-auth": "catalog:",
|
||||
"burnt": "0.13.0",
|
||||
"expo": "55.0.8",
|
||||
"expo-application": "55.0.10",
|
||||
"expo-build-properties": "55.0.10",
|
||||
"expo-clipboard": "55.0.9",
|
||||
"expo-constants": "55.0.9",
|
||||
"expo-dev-client": "55.0.18",
|
||||
"expo-device": "55.0.10",
|
||||
"expo-font": "55.0.4",
|
||||
"expo-glass-effect": "55.0.8",
|
||||
"expo-haptics": "55.0.9",
|
||||
"expo-image": "55.0.6",
|
||||
"expo-image-picker": "55.0.13",
|
||||
"expo-linear-gradient": "55.0.9",
|
||||
"expo-linking": "55.0.8",
|
||||
"expo-localization": "55.0.9",
|
||||
"expo-network": "55.0.9",
|
||||
"expo-router": "55.0.7",
|
||||
"expo-secure-store": "55.0.9",
|
||||
"expo-splash-screen": "55.0.12",
|
||||
"expo-status-bar": "55.0.4",
|
||||
"expo-system-ui": "55.0.10",
|
||||
"expo-updates": "55.0.15",
|
||||
"expo-web-browser": "55.0.10",
|
||||
"expo-widgets": "55.0.7",
|
||||
"posthog-react-native": "4.37.6",
|
||||
"expo": "~57.0.26",
|
||||
"expo-application": "~57.0.3",
|
||||
"expo-clipboard": "~57.0.2",
|
||||
"expo-constants": "~57.0.20",
|
||||
"expo-dev-client": "~57.0.19",
|
||||
"expo-device": "~57.0.2",
|
||||
"expo-font": "~57.0.4",
|
||||
"expo-glass-effect": "~57.0.4",
|
||||
"expo-haptics": "~57.0.3",
|
||||
"expo-image": "~57.0.5",
|
||||
"expo-image-picker": "~57.0.20",
|
||||
"expo-linear-gradient": "~57.0.2",
|
||||
"expo-linking": "~57.0.11",
|
||||
"expo-localization": "~57.0.2",
|
||||
"expo-network": "~57.0.2",
|
||||
"expo-router": "~57.0.24",
|
||||
"expo-secure-store": "~57.0.4",
|
||||
"expo-splash-screen": "~57.0.9",
|
||||
"expo-status-bar": "~57.0.1",
|
||||
"expo-system-ui": "~57.0.4",
|
||||
"expo-updates": "~57.0.24",
|
||||
"expo-web-browser": "~57.0.3",
|
||||
"expo-widgets": "~57.0.22",
|
||||
"posthog-react-native": "4.78.4",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-native": "0.83.2",
|
||||
"react-native-gesture-handler": "2.30.0",
|
||||
"react-native-ios-context-menu": "3.2.0",
|
||||
"react-native-ios-utilities": "5.2.0",
|
||||
"react-native-keyboard-controller": "1.20.7",
|
||||
"react-native-mmkv": "4.2.0",
|
||||
"react-native-nitro-modules": "0.35.0",
|
||||
"react-native-reanimated": "4.2.1",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
"react-native-screens": "4.23.0",
|
||||
"react-native-svg": "15.15.3",
|
||||
"react-native-worklets": "0.7.2",
|
||||
"react-native": "0.86.3",
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
"react-native-keyboard-controller": "1.21.9",
|
||||
"react-native-mmkv": "~4.3.2",
|
||||
"react-native-nitro-modules": "~0.37.1",
|
||||
"react-native-reanimated": "4.5.1",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "~4.26.0",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-worklets": "0.10.1",
|
||||
"tailwind-merge": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"uniwind": "1.6.1",
|
||||
"zeego": "3.0.6",
|
||||
"uniwind": "1.12.1",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -95,5 +89,13 @@
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"expo": {
|
||||
"install": {
|
||||
"exclude": [
|
||||
"@sentry/react-native",
|
||||
"typescript"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,8 +99,8 @@ export default function LoginScreen() {
|
||||
<Animated.View entering={FadeInDown.duration(300).delay(100)} className="mb-4">
|
||||
<Button
|
||||
onPress={() => {
|
||||
authClient.signIn.oauth2({
|
||||
providerId: "oidc",
|
||||
authClient.signIn.social({
|
||||
provider: "oidc",
|
||||
callbackURL: "/(tabs)/(home)",
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -27,6 +27,20 @@ function LibraryHeaderRight() {
|
||||
);
|
||||
}
|
||||
|
||||
function renderLibraryHeaderRight() {
|
||||
return <LibraryHeaderRight />;
|
||||
}
|
||||
|
||||
function getLibraryHeaderRightItems() {
|
||||
return [
|
||||
{
|
||||
type: "custom" as const,
|
||||
element: <LibraryHeaderRight />,
|
||||
hidesSharedBackground: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function LibraryLayout() {
|
||||
const { t } = useLingui();
|
||||
const contentStyle = useResolveClassNames("bg-background");
|
||||
@@ -40,13 +54,7 @@ export default function LibraryLayout() {
|
||||
<Stack
|
||||
screenOptions={{
|
||||
contentStyle,
|
||||
unstable_headerRightItems: () => [
|
||||
{
|
||||
type: "custom" as const,
|
||||
element: <LibraryHeaderRight />,
|
||||
hidesSharedBackground: true,
|
||||
},
|
||||
],
|
||||
unstable_headerRightItems: getLibraryHeaderRightItems,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index">
|
||||
@@ -76,7 +84,7 @@ export default function LibraryLayout() {
|
||||
screenOptions={{
|
||||
contentStyle,
|
||||
headerTitleAlign: "left",
|
||||
headerRight: () => <LibraryHeaderRight />,
|
||||
headerRight: renderLibraryHeaderRight,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Icon } from "@expo/ui";
|
||||
import { MenuView } from "@expo/ui/community/menu";
|
||||
import { plural } from "@lingui/core/macro";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import {
|
||||
@@ -38,7 +40,6 @@ import {
|
||||
} from "react-native";
|
||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||
import { useCSSVariable } from "uniwind";
|
||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||
|
||||
import { IntegrationsSection } from "@/components/settings/integrations-section";
|
||||
import { SettingsRow } from "@/components/settings/settings-row";
|
||||
@@ -66,6 +67,15 @@ const settingsContentContainerStyle = {
|
||||
paddingHorizontal: 16,
|
||||
};
|
||||
|
||||
const changePhotoIcon = Icon.select({
|
||||
ios: "photo.on.rectangle.angled",
|
||||
android: import("@expo/material-symbols/photo_library.xml"),
|
||||
});
|
||||
const removePhotoIcon = Icon.select({
|
||||
ios: "trash",
|
||||
android: import("@expo/material-symbols/delete.xml"),
|
||||
});
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const { t, i18n } = useLingui();
|
||||
const { push } = useRouter();
|
||||
@@ -241,52 +251,49 @@ export default function SettingsScreen() {
|
||||
<SettingsSection title={t`Account`} icon={IconUser}>
|
||||
<View className="flex-row items-center py-3.5">
|
||||
{hasAvatarImage ? (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t`Edit profile photo`}
|
||||
accessibilityHint={t`Opens options to change or remove your photo`}
|
||||
className="mr-3"
|
||||
hitSlop={8}
|
||||
>
|
||||
<View className="bg-secondary size-11 overflow-hidden rounded-full">
|
||||
{isUploadingAvatar ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<Spinner size="sm" colorClassName="accent-primary" />
|
||||
</View>
|
||||
) : (
|
||||
<Image
|
||||
source={{ uri: session.user.image ?? undefined }}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
contentFit="cover"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View className="bg-primary absolute right-0 bottom-0 size-[18px] items-center justify-center rounded-full">
|
||||
<IconCamera size={10} color={primaryFgColor} />
|
||||
</View>
|
||||
</Pressable>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item key="change" onSelect={() => pickAvatar()}>
|
||||
<DropdownMenu.ItemIcon ios={{ name: "photo.on.rectangle.angled" }} />
|
||||
<DropdownMenu.ItemTitle>
|
||||
<Trans>Change Photo</Trans>
|
||||
</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
key="remove"
|
||||
destructive
|
||||
onSelect={() => removeAvatar.mutate()}
|
||||
>
|
||||
<DropdownMenu.ItemIcon ios={{ name: "trash" }} />
|
||||
<DropdownMenu.ItemTitle>
|
||||
<Trans>Remove Photo</Trans>
|
||||
</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<MenuView
|
||||
actions={[
|
||||
{ id: "change", title: t`Change Photo`, image: changePhotoIcon },
|
||||
{
|
||||
id: "remove",
|
||||
title: t`Remove Photo`,
|
||||
image: removePhotoIcon,
|
||||
attributes: { destructive: true },
|
||||
},
|
||||
]}
|
||||
onPressAction={({ nativeEvent }) => {
|
||||
if (nativeEvent.event === "change") {
|
||||
pickAvatar();
|
||||
} else if (nativeEvent.event === "remove") {
|
||||
removeAvatar.mutate();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t`Edit profile photo`}
|
||||
accessibilityHint={t`Opens options to change or remove your photo`}
|
||||
className="mr-3"
|
||||
hitSlop={8}
|
||||
>
|
||||
<View className="bg-secondary size-11 overflow-hidden rounded-full">
|
||||
{isUploadingAvatar ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<Spinner size="sm" colorClassName="accent-primary" />
|
||||
</View>
|
||||
) : (
|
||||
<Image
|
||||
source={{ uri: session.user.image ?? undefined }}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
contentFit="cover"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View className="bg-primary absolute right-0 bottom-0 size-[18px] items-center justify-center rounded-full">
|
||||
<IconCamera size={10} color={primaryFgColor} />
|
||||
</View>
|
||||
</Pressable>
|
||||
</MenuView>
|
||||
) : (
|
||||
<Pressable
|
||||
onPress={pickAvatar}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import "@/lib/intl-polyfills";
|
||||
import "@/global.css";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { ThemeProvider } from "@react-navigation/native";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import {
|
||||
persistQueryClientRestore,
|
||||
persistQueryClientSubscribe,
|
||||
} from "@tanstack/react-query-persist-client";
|
||||
import { Stack, useGlobalSearchParams, usePathname } from "expo-router";
|
||||
import { ThemeProvider } from "expo-router/react-navigation";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { PostHogErrorBoundary, PostHogProvider } from "posthog-react-native";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { useHeaderHeight } from "@react-navigation/elements";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
} from "@tabler/icons-react-native";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useHeaderHeight } from "expo-router/react-navigation";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { Pressable, useWindowDimensions, View } from "react-native";
|
||||
import Animated, { FadeIn, FadeInDown } from "react-native-reanimated";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { useHeaderHeight } from "@react-navigation/elements";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
@@ -18,6 +17,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { Link, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useHeaderHeight } from "expo-router/react-navigation";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { Pressable, ScrollView, StyleSheet, View } from "react-native";
|
||||
@@ -57,10 +57,10 @@ const titleGenresContentStyle = { paddingHorizontal: 16 };
|
||||
const titleAvailabilityContentStyle = { gap: 8, paddingHorizontal: 16 };
|
||||
const titleDetailStyles = StyleSheet.create({
|
||||
heroFill: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
},
|
||||
heroBaseOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: "rgba(0,0,0,0.1)",
|
||||
},
|
||||
heroGradient: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { StyleProp, ViewStyle } from "react-native";
|
||||
import { ScrollView } from "react-native";
|
||||
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
|
||||
import Animated, { FadeIn } from "react-native-reanimated";
|
||||
import type { AnimatedStyle } from "react-native-reanimated";
|
||||
|
||||
import { SofaLogo } from "@/components/ui/sofa-logo";
|
||||
import { Text } from "@/components/ui/text";
|
||||
@@ -10,7 +11,7 @@ import { Text } from "@/components/ui/text";
|
||||
interface AuthScreenProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
logoStyle?: StyleProp<ViewStyle>;
|
||||
logoStyle?: StyleProp<AnimatedStyle<ViewStyle>>;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MenuView } from "@expo/ui/community/menu";
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import type { Icon } from "@tabler/icons-react-native";
|
||||
import { Pressable, View } from "react-native";
|
||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||
|
||||
import { ScaledIcon } from "@/components/ui/scaled-icon";
|
||||
import { Sparkline } from "@/components/ui/sparkline";
|
||||
@@ -86,29 +86,24 @@ export function StatsCard(props: StatsCardProps) {
|
||||
|
||||
return (
|
||||
<View style={cardStyle}>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Pressable
|
||||
onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<CardInner {...props} />
|
||||
</Pressable>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{periods.map((p) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={p}
|
||||
value={p === period ? "on" : "off"}
|
||||
onValueChange={() => onPeriodChange(p)}
|
||||
>
|
||||
<DropdownMenu.ItemIndicator />
|
||||
<DropdownMenu.ItemTitle>{periodLabels[p]}</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<MenuView
|
||||
actions={periods.map((value) => ({
|
||||
id: value,
|
||||
title: periodLabels[value],
|
||||
state: value === period ? "on" : "off",
|
||||
}))}
|
||||
onPressAction={({ nativeEvent }) => {
|
||||
const nextPeriod = periods.find((value) => value === nativeEvent.event);
|
||||
if (nextPeriod) onPeriodChange(nextPeriod);
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<CardInner {...props} />
|
||||
</Pressable>
|
||||
</MenuView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Icon } from "@expo/ui";
|
||||
import { MenuView } from "@expo/ui/community/menu";
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import { useRouter } from "expo-router";
|
||||
import { Alert, Pressable, View } from "react-native";
|
||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||
|
||||
import { Image } from "@/components/ui/image";
|
||||
import { Text } from "@/components/ui/text";
|
||||
@@ -9,6 +10,15 @@ import { queryClient } from "@/lib/query-client";
|
||||
import { authClient } from "@/lib/server";
|
||||
import * as Haptics from "@/utils/haptics";
|
||||
|
||||
const settingsIcon = Icon.select({
|
||||
ios: "gear",
|
||||
android: import("@expo/material-symbols/settings.xml"),
|
||||
});
|
||||
const signOutIcon = Icon.select({
|
||||
ios: "rectangle.portrait.and.arrow.right",
|
||||
android: import("@expo/material-symbols/logout.xml"),
|
||||
});
|
||||
|
||||
export function HeaderAvatar() {
|
||||
const { t } = useLingui();
|
||||
const { data: session } = authClient.useSession();
|
||||
@@ -19,66 +29,60 @@ export function HeaderAvatar() {
|
||||
const { user } = session;
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Pressable
|
||||
onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t`User menu`}
|
||||
hitSlop={8}
|
||||
>
|
||||
<View className="size-8 overflow-hidden rounded-full" accessible={false}>
|
||||
{user.image ? (
|
||||
<Image
|
||||
source={{ uri: user.image }}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
contentFit="cover"
|
||||
accessible={false}
|
||||
/>
|
||||
) : (
|
||||
<View className="bg-primary/[0.08] flex-1 items-center justify-center">
|
||||
<Text
|
||||
maxFontSizeMultiplier={1.0}
|
||||
className="font-display text-primary text-sm font-medium"
|
||||
>
|
||||
{user.name?.charAt(0)?.toUpperCase() ?? "?"}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item key="settings" onSelect={() => navigate("/(tabs)/(settings)")}>
|
||||
<DropdownMenu.ItemIcon ios={{ name: "gear" }} androidIconName="ic_menu_preferences" />
|
||||
<DropdownMenu.ItemTitle>{t`Settings`}</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
key="sign-out"
|
||||
destructive
|
||||
onSelect={() => {
|
||||
Alert.alert(t`Sign out`, t`Are you sure you want to sign out?`, [
|
||||
{ text: t`Cancel`, style: "cancel" },
|
||||
{
|
||||
text: t`Sign out`,
|
||||
style: "destructive",
|
||||
onPress: () => {
|
||||
authClient.signOut();
|
||||
queryClient.clear();
|
||||
},
|
||||
<MenuView
|
||||
actions={[
|
||||
{ id: "settings", title: t`Settings`, image: settingsIcon },
|
||||
{
|
||||
id: "sign-out",
|
||||
title: t`Sign out`,
|
||||
image: signOutIcon,
|
||||
attributes: { destructive: true },
|
||||
},
|
||||
]}
|
||||
onPressAction={({ nativeEvent }) => {
|
||||
if (nativeEvent.event === "settings") {
|
||||
navigate("/(tabs)/(settings)");
|
||||
} else if (nativeEvent.event === "sign-out") {
|
||||
Alert.alert(t`Sign out`, t`Are you sure you want to sign out?`, [
|
||||
{ text: t`Cancel`, style: "cancel" },
|
||||
{
|
||||
text: t`Sign out`,
|
||||
style: "destructive",
|
||||
onPress: () => {
|
||||
authClient.signOut();
|
||||
queryClient.clear();
|
||||
},
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemIcon
|
||||
ios={{ name: "rectangle.portrait.and.arrow.right" }}
|
||||
androidIconName="ic_menu_close_clear_cancel"
|
||||
/>
|
||||
<DropdownMenu.ItemTitle>{t`Sign out`}</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
},
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t`User menu`}
|
||||
hitSlop={8}
|
||||
>
|
||||
<View className="size-8 overflow-hidden rounded-full" accessible={false}>
|
||||
{user.image ? (
|
||||
<Image
|
||||
source={{ uri: user.image }}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
contentFit="cover"
|
||||
accessible={false}
|
||||
/>
|
||||
) : (
|
||||
<View className="bg-primary/[0.08] flex-1 items-center justify-center">
|
||||
<Text
|
||||
maxFontSizeMultiplier={1.0}
|
||||
className="font-display text-primary text-sm font-medium"
|
||||
>
|
||||
{user.name?.charAt(0)?.toUpperCase() ?? "?"}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</MenuView>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MenuView } from "@expo/ui/community/menu";
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import { IconArrowsSort } from "@tabler/icons-react-native";
|
||||
import { Pressable } from "react-native";
|
||||
import { useCSSVariable } from "uniwind";
|
||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||
|
||||
import { ScaledIcon } from "@/components/ui/scaled-icon";
|
||||
import * as Haptics from "@/utils/haptics";
|
||||
@@ -37,33 +37,27 @@ export function SortMenu({ sortBy, sortDirection, onSortChange }: SortMenuProps)
|
||||
];
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Pressable
|
||||
onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t`Sort`}
|
||||
hitSlop={8}
|
||||
>
|
||||
<ScaledIcon icon={IconArrowsSort} size={22} color={foregroundColor} />
|
||||
</Pressable>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{sortOptions.map((option) => {
|
||||
const isSelected = option.sortBy === sortBy && option.sortDirection === sortDirection;
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={`${option.sortBy}-${option.sortDirection}`}
|
||||
value={isSelected ? "on" : "off"}
|
||||
onValueChange={() => onSortChange(option.sortBy, option.sortDirection)}
|
||||
>
|
||||
<DropdownMenu.ItemIndicator />
|
||||
<DropdownMenu.ItemTitle>{option.label}</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<MenuView
|
||||
actions={sortOptions.map((option) => ({
|
||||
id: `${option.sortBy}-${option.sortDirection}`,
|
||||
title: option.label,
|
||||
state: option.sortBy === sortBy && option.sortDirection === sortDirection ? "on" : "off",
|
||||
}))}
|
||||
onPressAction={({ nativeEvent }) => {
|
||||
const option = sortOptions.find(
|
||||
(candidate) => `${candidate.sortBy}-${candidate.sortDirection}` === nativeEvent.event,
|
||||
);
|
||||
if (option) onSortChange(option.sortBy, option.sortDirection);
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t`Sort`}
|
||||
hitSlop={8}
|
||||
>
|
||||
<ScaledIcon icon={IconArrowsSort} size={22} color={foregroundColor} />
|
||||
</Pressable>
|
||||
</MenuView>
|
||||
);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export function NativeTabBar() {
|
||||
<NativeTabs.Trigger.Label>{t`Settings`}</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger.Icon sf="gear" md="settings" />
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="(search)" role="search" disableTransparentOnScrollEdge>
|
||||
<NativeTabs.Trigger name="(search)" disableTransparentOnScrollEdge>
|
||||
<NativeTabs.Trigger.Label>{t`Search`}</NativeTabs.Trigger.Label>
|
||||
<NativeTabs.Trigger.Icon sf="magnifyingglass" md="search" />
|
||||
</NativeTabs.Trigger>
|
||||
|
||||
@@ -4,6 +4,20 @@ import { useCSSVariable, useResolveClassNames } from "uniwind";
|
||||
|
||||
import { HeaderAvatar } from "@/components/header-avatar";
|
||||
|
||||
function renderHeaderAvatar() {
|
||||
return <HeaderAvatar />;
|
||||
}
|
||||
|
||||
function getHeaderRightItems() {
|
||||
return [
|
||||
{
|
||||
type: "custom" as const,
|
||||
element: <HeaderAvatar />,
|
||||
hidesSharedBackground: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function TabStack({ title, children }: { title?: string; children?: ReactNode }) {
|
||||
const contentStyle = useResolveClassNames("bg-background");
|
||||
const tintColor = useCSSVariable("--color-primary") as string;
|
||||
@@ -16,13 +30,7 @@ export function TabStack({ title, children }: { title?: string; children?: React
|
||||
<Stack
|
||||
screenOptions={{
|
||||
contentStyle,
|
||||
unstable_headerRightItems: () => [
|
||||
{
|
||||
type: "custom" as const,
|
||||
element: <HeaderAvatar />,
|
||||
hidesSharedBackground: true,
|
||||
},
|
||||
],
|
||||
unstable_headerRightItems: getHeaderRightItems,
|
||||
}}
|
||||
>
|
||||
{title ? (
|
||||
@@ -55,7 +63,7 @@ export function TabStack({ title, children }: { title?: string; children?: React
|
||||
screenOptions={{
|
||||
contentStyle,
|
||||
headerTitleAlign: "left",
|
||||
headerRight: () => <HeaderAvatar />,
|
||||
headerRight: renderHeaderAvatar,
|
||||
}}
|
||||
>
|
||||
{title ? (
|
||||
|
||||
@@ -51,18 +51,18 @@ export function SeasonAccordion({
|
||||
const [visibleCount, setVisibleCount] = useState(INITIAL_BATCH);
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded) {
|
||||
setVisibleCount(INITIAL_BATCH);
|
||||
if (episodes.length > INITIAL_BATCH) {
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
setVisibleCount(episodes.length);
|
||||
});
|
||||
return () => task.cancel();
|
||||
}
|
||||
if (expanded && episodes.length > INITIAL_BATCH) {
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
setVisibleCount(episodes.length);
|
||||
});
|
||||
return () => task.cancel();
|
||||
}
|
||||
}, [expanded, episodes.length]);
|
||||
|
||||
const toggleExpanded = useCallback(() => setExpanded((v) => !v), []);
|
||||
const toggleExpanded = useCallback(() => {
|
||||
if (!expanded) setVisibleCount(INITIAL_BATCH);
|
||||
setExpanded((v) => !v);
|
||||
}, [expanded]);
|
||||
|
||||
useEffect(() => {
|
||||
chevronRotation.set(
|
||||
|
||||
+14
-14
@@ -5,33 +5,33 @@ import { createMMKV } from "react-native-mmkv";
|
||||
export const globalStorage = createMMKV();
|
||||
|
||||
// Server+user scoped storage — switches when scope changes
|
||||
let _scopedStore: ReturnType<typeof createMMKV> | null = null;
|
||||
let _currentScopeKey: string | null = null;
|
||||
let scopedStore: ReturnType<typeof createMMKV> | null = null;
|
||||
let currentScopeKey: string | null = null;
|
||||
|
||||
const scopeChangeListeners: Array<() => void> = [];
|
||||
|
||||
export function scopedStorage() {
|
||||
if (!_scopedStore) throw new Error("Scoped storage not initialized");
|
||||
return _scopedStore;
|
||||
if (!scopedStore) throw new Error("Scoped storage not initialized");
|
||||
return scopedStore;
|
||||
}
|
||||
|
||||
export function hasScopedStorage(): boolean {
|
||||
return _scopedStore !== null;
|
||||
return scopedStore !== null;
|
||||
}
|
||||
|
||||
export function getScopeKey(): string | null {
|
||||
return _currentScopeKey;
|
||||
return currentScopeKey;
|
||||
}
|
||||
|
||||
export function setStorageScope(instanceId: string, userId: string) {
|
||||
_currentScopeKey = `${instanceId}_${userId}`;
|
||||
_scopedStore = createMMKV({ id: _currentScopeKey });
|
||||
currentScopeKey = `${instanceId}_${userId}`;
|
||||
scopedStore = createMMKV({ id: currentScopeKey });
|
||||
for (const listener of scopeChangeListeners) listener();
|
||||
}
|
||||
|
||||
export function clearStorageScope() {
|
||||
_currentScopeKey = null;
|
||||
_scopedStore = null;
|
||||
currentScopeKey = null;
|
||||
scopedStore = null;
|
||||
for (const listener of scopeChangeListeners) listener();
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ export function onStorageScopeChange(callback: () => void): () => void {
|
||||
};
|
||||
}
|
||||
|
||||
// Query persister — reads/writes through _scopedStore via closure
|
||||
// Query persister — reads/writes through scopedStore via closure
|
||||
const scopedMmkvStorage = {
|
||||
getItem: (key: string) => _scopedStore?.getString(key) ?? null,
|
||||
setItem: (key: string, value: string) => _scopedStore?.set(key, value),
|
||||
removeItem: (key: string) => void _scopedStore?.remove(key),
|
||||
getItem: (key: string) => scopedStore?.getString(key) ?? null,
|
||||
setItem: (key: string, value: string) => scopedStore?.set(key, value),
|
||||
removeItem: (key: string) => void scopedStore?.remove(key),
|
||||
};
|
||||
|
||||
export const QUERY_CACHE_KEY = "REACT_QUERY_OFFLINE_CACHE";
|
||||
|
||||
@@ -13,12 +13,12 @@ export const link = new RPCLink({
|
||||
...options,
|
||||
credentials: process.env.EXPO_OS === "web" ? "include" : "omit",
|
||||
}),
|
||||
headers() {
|
||||
async headers() {
|
||||
if (process.env.EXPO_OS === "web") {
|
||||
return {};
|
||||
}
|
||||
const headers = new Map<string, string>();
|
||||
const cookies = authClient.getCookie();
|
||||
const cookies = await authClient.getCookie();
|
||||
if (cookies) {
|
||||
headers.set("Cookie", cookies);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expoClient } from "@better-auth/expo/client";
|
||||
import { focusManager, onlineManager } from "@tanstack/react-query";
|
||||
import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
|
||||
import { adminClient } from "better-auth/client/plugins";
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
import * as Network from "expo-network";
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
@@ -336,7 +336,6 @@ function buildAuthClient() {
|
||||
},
|
||||
plugins: [
|
||||
adminClient(),
|
||||
genericOAuthClient(),
|
||||
expoClient({
|
||||
scheme: "sofa",
|
||||
storagePrefix: getStoragePrefix(),
|
||||
@@ -380,7 +379,7 @@ function getCachedSession(): CachedSessionData | null {
|
||||
}
|
||||
}
|
||||
|
||||
let _cachedSessionSeeded = false;
|
||||
let cachedSessionSeeded = false;
|
||||
|
||||
/**
|
||||
* Seed the Better Auth session atom from SecureStore before React renders.
|
||||
@@ -389,7 +388,7 @@ let _cachedSessionSeeded = false;
|
||||
export function initSession(): void {
|
||||
const cached = getCachedSession();
|
||||
if (cached) {
|
||||
_cachedSessionSeeded = true;
|
||||
cachedSessionSeeded = true;
|
||||
const sessionAtom = authClient.$store.atoms.session;
|
||||
sessionAtom.set({
|
||||
data: cached,
|
||||
@@ -402,11 +401,11 @@ export function initSession(): void {
|
||||
}
|
||||
|
||||
export function wasCachedSessionSeeded(): boolean {
|
||||
return _cachedSessionSeeded;
|
||||
return cachedSessionSeeded;
|
||||
}
|
||||
|
||||
export function clearCachedSessionSeeded(): void {
|
||||
_cachedSessionSeeded = false;
|
||||
cachedSessionSeeded = false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -419,15 +418,15 @@ export function clearCachedSessionSeeded(): void {
|
||||
|
||||
// Signals that the next session-loss redirect should go to the server-url
|
||||
// screen instead of login (set by the "Change Server" flow in settings).
|
||||
let _serverChangeRequested = false;
|
||||
let serverChangeRequested = false;
|
||||
|
||||
export function requestServerChange(): void {
|
||||
_serverChangeRequested = true;
|
||||
serverChangeRequested = true;
|
||||
}
|
||||
|
||||
export function consumeServerChangeRequest(): boolean {
|
||||
const was = _serverChangeRequested;
|
||||
_serverChangeRequested = false;
|
||||
const was = serverChangeRequested;
|
||||
serverChangeRequested = false;
|
||||
return was;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DarkTheme } from "@react-navigation/native";
|
||||
import { DarkTheme } from "expo-router/react-navigation";
|
||||
|
||||
export const sofaTheme = {
|
||||
...DarkTheme,
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
const CACHED_WIDGET_IMAGE_RE = /^file:\/\/\/group\/cw_/;
|
||||
type LibraryResult = { items: Array<Record<string, unknown>> };
|
||||
type SnapshotProps = Record<string, unknown>;
|
||||
type TimelineEntry = { props?: Record<string, unknown> };
|
||||
|
||||
const platform = { OS: "ios" };
|
||||
const resolveAssetSource = vi.fn(() => ({ uri: "file:///tmp/sofa-icon.png" }));
|
||||
const continueWatching = vi.fn();
|
||||
const upcoming = vi.fn();
|
||||
const downloadWidgetImage = vi.fn(async (_url: string, key: string) => `file:///group/${key}`);
|
||||
const copyBundledAsset = vi.fn(async (_assetUri: string, key: string) => `file:///group/${key}`);
|
||||
const pruneWidgetImages = vi.fn(async () => undefined);
|
||||
const getWidgetIconAsset = vi.fn(() => "widget-icon-asset");
|
||||
const resolveAssetSource = vi.fn<() => { uri: string }>(() => ({
|
||||
uri: "file:///tmp/sofa-icon.png",
|
||||
}));
|
||||
const continueWatching = vi.fn<() => Promise<LibraryResult>>();
|
||||
const upcoming = vi.fn<() => Promise<LibraryResult>>();
|
||||
const downloadWidgetImage = vi.fn<(url: string, key: string) => Promise<string>>(
|
||||
async (_url: string, key: string) => `file:///group/${key}`,
|
||||
);
|
||||
const copyBundledAsset = vi.fn<(assetUri: string, key: string) => Promise<string>>(
|
||||
async (_assetUri: string, key: string) => `file:///group/${key}`,
|
||||
);
|
||||
const pruneWidgetImages = vi.fn<() => Promise<void>>(async () => undefined);
|
||||
const getWidgetIconAsset = vi.fn<() => string>(() => "widget-icon-asset");
|
||||
|
||||
const continueWatchingWidget = {
|
||||
updateSnapshot: vi.fn(),
|
||||
updateTimeline: vi.fn(),
|
||||
updateSnapshot: vi.fn<(props: SnapshotProps) => void>(),
|
||||
updateTimeline: vi.fn<(entries: TimelineEntry[]) => void>(),
|
||||
};
|
||||
|
||||
const upcomingWidget = {
|
||||
updateSnapshot: vi.fn(),
|
||||
updateTimeline: vi.fn(),
|
||||
updateSnapshot: vi.fn<(props: SnapshotProps) => void>(),
|
||||
updateTimeline: vi.fn<(entries: TimelineEntry[]) => void>(),
|
||||
};
|
||||
|
||||
vi.mock("react-native", () => ({
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/zod-validator": "0.7.6",
|
||||
"@vercel/firewall": "1.1.2",
|
||||
"hono": "4.12.9",
|
||||
"@hono/zod-validator": "0.9.1",
|
||||
"@vercel/firewall": "1.2.5",
|
||||
"hono": "4.13.13",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"@sofa/db": "workspace:*",
|
||||
"@sofa/logger": "workspace:*",
|
||||
"@sofa/tmdb": "workspace:*",
|
||||
"croner": "10.0.2-dev.2",
|
||||
"hono": "4.12.9",
|
||||
"croner": "10.0.2-dev.3",
|
||||
"hono": "4.13.13",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -46,13 +46,13 @@ const DAY = 24 * 60 * 60 * 1000;
|
||||
const RATE_LIMIT_MS = 300;
|
||||
|
||||
const globalForJobs = globalThis as unknown as {
|
||||
_jobs: Map<string, Cron> | undefined;
|
||||
cronJobs: Map<string, Cron> | undefined;
|
||||
};
|
||||
|
||||
if (!globalForJobs._jobs) {
|
||||
globalForJobs._jobs = new Map<string, Cron>();
|
||||
if (!globalForJobs.cronJobs) {
|
||||
globalForJobs.cronJobs = new Map<string, Cron>();
|
||||
}
|
||||
const jobs = globalForJobs._jobs;
|
||||
const jobs = globalForJobs.cronJobs;
|
||||
|
||||
function schedule(name: string, cron: string, handler: () => Promise<void>) {
|
||||
jobs.set(
|
||||
|
||||
@@ -4,6 +4,15 @@ import { auth } from "@sofa/auth/server";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Better Auth >= 1.7 serves genericOAuth callbacks at the core `/callback/:id`
|
||||
// endpoint. Existing IdP registrations (and `getOidcRedirectURI()`) still use
|
||||
// the legacy `/oauth2/callback/:id` path, so forward it to the new endpoint.
|
||||
app.all("/oauth2/callback/:providerId", (c) => {
|
||||
const url = new URL(c.req.url);
|
||||
url.pathname = url.pathname.replace("/oauth2/callback/", "/callback/");
|
||||
return auth.handler(new Request(url.toString(), c.req.raw));
|
||||
});
|
||||
|
||||
app.all("/*", (c) => auth.handler(c.req.raw));
|
||||
|
||||
export default app;
|
||||
+21
-21
@@ -14,35 +14,35 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "1.3.0",
|
||||
"@fontsource-variable/dm-sans": "5.2.8",
|
||||
"@fontsource-variable/geist-mono": "5.2.7",
|
||||
"@fontsource/dm-serif-display": "5.2.8",
|
||||
"@base-ui/react": "1.8.0",
|
||||
"@fontsource-variable/dm-sans": "5.3.0",
|
||||
"@fontsource-variable/geist-mono": "5.3.0",
|
||||
"@fontsource/dm-serif-display": "5.3.0",
|
||||
"@lingui/core": "catalog:",
|
||||
"@lingui/react": "catalog:",
|
||||
"@orpc/client": "catalog:",
|
||||
"@orpc/contract": "catalog:",
|
||||
"@orpc/tanstack-query": "catalog:",
|
||||
"@player.style/sutro": "0.2.1",
|
||||
"@rolldown/plugin-babel": "0.2.2",
|
||||
"@rolldown/plugin-babel": "0.2.4",
|
||||
"@sofa/api": "workspace:*",
|
||||
"@sofa/i18n": "workspace:*",
|
||||
"@tabler/icons-react": "3.40.0",
|
||||
"@tabler/icons-react": "3.48.0",
|
||||
"@tanstack/react-form": "catalog:",
|
||||
"@tanstack/react-hotkeys": "0.8.1",
|
||||
"@tanstack/react-hotkeys": "0.13.0",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-router": "1.168.5",
|
||||
"@tanstack/zod-adapter": "1.166.9",
|
||||
"@tanstack/react-router": "1.170.41",
|
||||
"@tanstack/zod-adapter": "1.167.0",
|
||||
"better-auth": "catalog:",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"jotai": "2.19.0",
|
||||
"motion": "12.38.0",
|
||||
"jotai": "3.0.1",
|
||||
"motion": "14.0.0",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"shadcn": "4.1.0",
|
||||
"sonner": "2.0.7",
|
||||
"shadcn": "4.21.1",
|
||||
"sonner": "2.0.8",
|
||||
"tailwind-merge": "catalog:",
|
||||
"thumbhash": "catalog:",
|
||||
"tw-animate-css": "1.4.0",
|
||||
@@ -52,23 +52,23 @@
|
||||
"@lingui/babel-plugin-lingui-macro": "catalog:",
|
||||
"@lingui/vite-plugin": "catalog:",
|
||||
"@sofa/tsconfig": "workspace:*",
|
||||
"@tailwindcss/vite": "4.2.2",
|
||||
"@tanstack/devtools-vite": "0.6.0",
|
||||
"@tanstack/react-devtools": "0.10.0",
|
||||
"@tanstack/react-query-devtools": "5.95.2",
|
||||
"@tanstack/react-router-devtools": "1.166.11",
|
||||
"@tanstack/router-plugin": "1.167.6",
|
||||
"@tailwindcss/vite": "4.3.3",
|
||||
"@tanstack/devtools-vite": "0.8.5",
|
||||
"@tanstack/react-devtools": "0.10.13",
|
||||
"@tanstack/react-query-devtools": "5.104.1",
|
||||
"@tanstack/react-router-devtools": "1.167.2",
|
||||
"@tanstack/router-plugin": "1.168.42",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "6.0.1",
|
||||
"@vitejs/plugin-react": "6.1.1",
|
||||
"@vitest/browser": "catalog:",
|
||||
"@vitest/browser-playwright": "catalog:",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "8.0.3",
|
||||
"vite": "8.3.2",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -81,8 +81,8 @@ export function AuthForm({
|
||||
setError("");
|
||||
setOidcLoading(true);
|
||||
try {
|
||||
await authClient.signIn.oauth2({
|
||||
providerId: "oidc",
|
||||
await authClient.signIn.social({
|
||||
provider: "oidc",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
} catch {
|
||||
|
||||
@@ -25,6 +25,8 @@ interface TitleRowItem {
|
||||
}
|
||||
|
||||
type TitleStatus = "in_watchlist" | "watching" | "caught_up" | "completed";
|
||||
const EMPTY_STATUSES: Record<string, TitleStatus> = {};
|
||||
const EMPTY_PROGRESS: Record<string, { watched: number; total: number }> = {};
|
||||
|
||||
interface FilterableTitleRowProps {
|
||||
heading: string;
|
||||
@@ -42,8 +44,8 @@ export function FilterableTitleRow({
|
||||
mediaType,
|
||||
defaultItems,
|
||||
genres,
|
||||
userStatuses: initialStatuses = {},
|
||||
episodeProgress: initialProgress = {},
|
||||
userStatuses: initialStatuses = EMPTY_STATUSES,
|
||||
episodeProgress: initialProgress = EMPTY_PROGRESS,
|
||||
}: FilterableTitleRowProps) {
|
||||
const [selectedGenre, setSelectedGenre] = useState<number | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
import type { ErrorComponentProps } from "@tanstack/react-router";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
export function RouteError({ reset }: ErrorComponentProps) {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-6 py-24 text-center">
|
||||
<div className="border-destructive/20 bg-destructive/5 text-destructive flex size-14 items-center justify-center rounded-full border">
|
||||
@@ -23,6 +25,7 @@ export function RouteError({ reset }: ErrorComponentProps) {
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t`Try again`}
|
||||
onClick={reset}
|
||||
className="group bg-primary text-primary-foreground hover:shadow-primary/20 relative inline-flex h-10 items-center justify-center gap-2 overflow-hidden rounded-lg px-5 text-sm font-medium transition-shadow hover:shadow-lg"
|
||||
>
|
||||
|
||||
@@ -171,6 +171,7 @@ export function AccountSection({
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
aria-label={avatarUrl ? t`Remove profile picture` : t`Upload profile picture`}
|
||||
onClick={avatarUrl ? handleRemoveAvatar : () => fileInputRef.current?.click()}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
@@ -178,7 +179,6 @@ export function AccountSection({
|
||||
/>
|
||||
}
|
||||
className="focus-visible:ring-ring focus-visible:ring-offset-background relative shrink-0 cursor-pointer rounded-full focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||
aria-label={avatarUrl ? t`Remove profile picture` : t`Upload profile picture`}
|
||||
>
|
||||
<Avatar className="size-12 overflow-hidden">
|
||||
<AvatarImage src={isAvatarPending ? undefined : avatarUrl} alt={displayName} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { IconCloudUpload, IconLink } from "@tabler/icons-react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -209,9 +209,6 @@ function ImportSourceCard({ config }: { config: SourceConfig }) {
|
||||
onError: (err) => {
|
||||
toast.error(getErrorMessage(err, t, t`Failed to parse file`));
|
||||
},
|
||||
onSettled: () => {
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -236,7 +233,14 @@ function ImportSourceCard({ config }: { config: SourceConfig }) {
|
||||
} | null>(null);
|
||||
|
||||
function handleFileSelect(file: File) {
|
||||
parseMutation.mutate({ source: config.source, file });
|
||||
parseMutation.mutate(
|
||||
{ source: config.source, file },
|
||||
{
|
||||
onSettled: () => {
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function handleImport() {
|
||||
@@ -323,13 +327,12 @@ function ImportSourceCard({ config }: { config: SourceConfig }) {
|
||||
}
|
||||
|
||||
// Clean up poll timer on unmount or dialog close
|
||||
const stopPollingRef = useRef(() => {
|
||||
const stopPolling = useCallback(() => {
|
||||
if (pollTimerRef.current) {
|
||||
clearInterval(pollTimerRef.current);
|
||||
pollTimerRef.current = null;
|
||||
}
|
||||
});
|
||||
const stopPolling = stopPollingRef.current;
|
||||
}, []);
|
||||
|
||||
function handleClose() {
|
||||
stopPolling();
|
||||
|
||||
@@ -4,7 +4,6 @@ export function SofaLogo({ className }: { className?: string }) {
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
className={className ?? "size-6"}
|
||||
role="img"
|
||||
aria-label="Sofa"
|
||||
>
|
||||
<path
|
||||
|
||||
@@ -125,7 +125,7 @@ function QuickAddButton({ id, userStatus }: { id: string; userStatus?: TitleStat
|
||||
<TooltipTrigger
|
||||
onClick={handleClick}
|
||||
className="absolute end-2 top-2 z-10 flex size-8 items-center justify-center rounded-full bg-black/50 text-white opacity-60 backdrop-blur-sm transition-opacity hover:bg-black/70 focus-visible:opacity-100 sm:opacity-0 sm:group-hover:opacity-100"
|
||||
render={<button type="button" />}
|
||||
render={<button type="button" aria-label={t`Add to Watchlist`} />}
|
||||
>
|
||||
{!addToWatchlistMutation.isPending && <IconPlus className="size-4" />}
|
||||
{addToWatchlistMutation.isPending && <IconLoader className="size-4 animate-spin" />}
|
||||
|
||||
@@ -22,7 +22,6 @@ export function StarRating({ value, onChange }: StarRatingProps) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-0.5"
|
||||
role="radiogroup"
|
||||
aria-label={t`Rating`}
|
||||
onMouseLeave={() => setHover(0)}
|
||||
>
|
||||
@@ -32,8 +31,7 @@ export function StarRating({ value, onChange }: StarRatingProps) {
|
||||
<motion.button
|
||||
key={star}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={star === value}
|
||||
aria-pressed={star === value}
|
||||
aria-label={t`Rate ${plural(star, { one: "# star", other: "# stars" })}`}
|
||||
onClick={() => onChange(star === value ? 0 : star)}
|
||||
onMouseEnter={() => setHover(star)}
|
||||
|
||||
@@ -4,7 +4,6 @@ export function TmdbLogo({ className }: { className?: string }) {
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 190.24 81.52"
|
||||
className={className}
|
||||
role="img"
|
||||
aria-label="TMDB"
|
||||
>
|
||||
<defs>
|
||||
|
||||
@@ -29,7 +29,6 @@ function ButtonGroup({
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
|
||||
@@ -71,7 +71,6 @@ function Field({
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="field"
|
||||
data-orientation={orientation}
|
||||
className={cn(fieldVariants({ orientation }), className)}
|
||||
|
||||
@@ -10,7 +10,6 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"group/input-group border-input bg-input/20 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:bg-input/30 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 relative flex h-7 w-full min-w-0 items-center rounded-md border transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-md has-data-[align=block-start]:rounded-md has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot][aria-invalid=true]]:ring-2 has-[textarea]:rounded-md has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pe-1.5 has-[>[data-align=inline-start]]:[&>input]:ps-1.5",
|
||||
className,
|
||||
@@ -44,7 +43,7 @@ function InputGroupAddon({
|
||||
align = "inline-start",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||
const focusInput = (e: React.SyntheticEvent<HTMLDivElement>) => {
|
||||
const focusInput = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if ((e.target as HTMLElement).closest("button")) {
|
||||
return;
|
||||
}
|
||||
@@ -52,16 +51,10 @@ function InputGroupAddon({
|
||||
};
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="input-group-addon"
|
||||
data-align={align}
|
||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||
onClick={focusInput}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
focusInput(e);
|
||||
}
|
||||
}}
|
||||
onPointerDown={focusInput}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,6 @@ import { cn } from "@/lib/utils";
|
||||
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
role="list"
|
||||
data-slot="item-group"
|
||||
className={cn(
|
||||
"group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2",
|
||||
|
||||
@@ -11,7 +11,7 @@ function Slider({
|
||||
max = 100,
|
||||
...props
|
||||
}: SliderPrimitive.Root.Props) {
|
||||
const _values = React.useMemo(
|
||||
const values = React.useMemo(
|
||||
() => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),
|
||||
[value, defaultValue, min, max],
|
||||
);
|
||||
@@ -37,7 +37,7 @@ function Slider({
|
||||
className="bg-primary select-none data-horizontal:h-full data-vertical:w-full"
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
{Array.from({ length: values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
|
||||
@@ -7,7 +7,6 @@ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
const { t } = useLingui();
|
||||
return (
|
||||
<IconLoader
|
||||
role="status"
|
||||
aria-label={t`Loading`}
|
||||
className={cn("size-4 animate-spin", className)}
|
||||
{...props}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
||||
const [isMobile, setIsMobile] = React.useState(() => window.innerWidth < MOBILE_BREAKPOINT);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
@@ -11,9 +11,8 @@ export function useIsMobile() {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
return isMobile;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
|
||||
import { adminClient } from "better-auth/client/plugins";
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [adminClient(), genericOAuthClient()],
|
||||
plugins: [adminClient()],
|
||||
});
|
||||
|
||||
export const { signIn, signUp, signOut, useSession } = authClient;
|
||||
+122
-122
@@ -9,67 +9,42 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as SetupRouteImport } from './routes/setup'
|
||||
import { Route as AuthRouteImport } from './routes/_auth'
|
||||
import { Route as AppRouteImport } from './routes/_app'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AuthRegisterRouteImport } from './routes/_auth/register'
|
||||
import { Route as AuthLoginRouteImport } from './routes/_auth/login'
|
||||
import { Route as AppUpcomingRouteImport } from './routes/_app/upcoming'
|
||||
import { Route as AppSettingsRouteImport } from './routes/_app/settings'
|
||||
import { Route as AppOnboardingRouteImport } from './routes/_app/onboarding'
|
||||
import { Route as AppLibraryRouteImport } from './routes/_app/library'
|
||||
import { Route as AppExploreRouteImport } from './routes/_app/explore'
|
||||
import { Route as AppRouteImport } from './routes/_app'
|
||||
import { Route as AuthRouteImport } from './routes/_auth'
|
||||
import { Route as SetupRouteImport } from './routes/setup'
|
||||
import { Route as AppDashboardRouteImport } from './routes/_app/dashboard'
|
||||
import { Route as AppTitlesIdRouteImport } from './routes/_app/titles.$id'
|
||||
import { Route as AppExploreRouteImport } from './routes/_app/explore'
|
||||
import { Route as AppLibraryRouteImport } from './routes/_app/library'
|
||||
import { Route as AppOnboardingRouteImport } from './routes/_app/onboarding'
|
||||
import { Route as AppSettingsRouteImport } from './routes/_app/settings'
|
||||
import { Route as AppUpcomingRouteImport } from './routes/_app/upcoming'
|
||||
import { Route as AuthLoginRouteImport } from './routes/_auth/login'
|
||||
import { Route as AuthRegisterRouteImport } from './routes/_auth/register'
|
||||
import { Route as AppPeopleIdRouteImport } from './routes/_app/people.$id'
|
||||
import { Route as AppTitlesIdRouteImport } from './routes/_app/titles.$id'
|
||||
|
||||
const SetupRoute = SetupRouteImport.update({
|
||||
id: '/setup',
|
||||
path: '/setup',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthRoute = AuthRouteImport.update({
|
||||
id: '/_auth',
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AppRoute = AppRouteImport.update({
|
||||
id: '/_app',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
const AuthRoute = AuthRouteImport.update({
|
||||
id: '/_auth',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthRegisterRoute = AuthRegisterRouteImport.update({
|
||||
id: '/register',
|
||||
path: '/register',
|
||||
getParentRoute: () => AuthRoute,
|
||||
const SetupRoute = SetupRouteImport.update({
|
||||
id: '/setup',
|
||||
path: '/setup',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AuthLoginRoute = AuthLoginRouteImport.update({
|
||||
id: '/login',
|
||||
path: '/login',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AppUpcomingRoute = AppUpcomingRouteImport.update({
|
||||
id: '/upcoming',
|
||||
path: '/upcoming',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppSettingsRoute = AppSettingsRouteImport.update({
|
||||
id: '/settings',
|
||||
path: '/settings',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppOnboardingRoute = AppOnboardingRouteImport.update({
|
||||
id: '/onboarding',
|
||||
path: '/onboarding',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppLibraryRoute = AppLibraryRouteImport.update({
|
||||
id: '/library',
|
||||
path: '/library',
|
||||
const AppDashboardRoute = AppDashboardRouteImport.update({
|
||||
id: '/dashboard',
|
||||
path: '/dashboard',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppExploreRoute = AppExploreRouteImport.update({
|
||||
@@ -77,9 +52,39 @@ const AppExploreRoute = AppExploreRouteImport.update({
|
||||
path: '/explore',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppDashboardRoute = AppDashboardRouteImport.update({
|
||||
id: '/dashboard',
|
||||
path: '/dashboard',
|
||||
const AppLibraryRoute = AppLibraryRouteImport.update({
|
||||
id: '/library',
|
||||
path: '/library',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppOnboardingRoute = AppOnboardingRouteImport.update({
|
||||
id: '/onboarding',
|
||||
path: '/onboarding',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppSettingsRoute = AppSettingsRouteImport.update({
|
||||
id: '/settings',
|
||||
path: '/settings',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppUpcomingRoute = AppUpcomingRouteImport.update({
|
||||
id: '/upcoming',
|
||||
path: '/upcoming',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AuthLoginRoute = AuthLoginRouteImport.update({
|
||||
id: '/login',
|
||||
path: '/login',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthRegisterRoute = AuthRegisterRouteImport.update({
|
||||
id: '/register',
|
||||
path: '/register',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AppPeopleIdRoute = AppPeopleIdRouteImport.update({
|
||||
id: '/people/$id',
|
||||
path: '/people/$id',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppTitlesIdRoute = AppTitlesIdRouteImport.update({
|
||||
@@ -87,11 +92,6 @@ const AppTitlesIdRoute = AppTitlesIdRouteImport.update({
|
||||
path: '/titles/$id',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppPeopleIdRoute = AppPeopleIdRouteImport.update({
|
||||
id: '/people/$id',
|
||||
path: '/people/$id',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
@@ -194,18 +194,11 @@ export interface RootRouteChildren {
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/setup': {
|
||||
id: '/setup'
|
||||
path: '/setup'
|
||||
fullPath: '/setup'
|
||||
preLoaderRoute: typeof SetupRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_auth': {
|
||||
id: '/_auth'
|
||||
path: ''
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AuthRouteImport
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_app': {
|
||||
@@ -215,53 +208,25 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
'/_auth': {
|
||||
id: '/_auth'
|
||||
path: ''
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
preLoaderRoute: typeof AuthRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_auth/register': {
|
||||
id: '/_auth/register'
|
||||
path: '/register'
|
||||
fullPath: '/register'
|
||||
preLoaderRoute: typeof AuthRegisterRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
'/setup': {
|
||||
id: '/setup'
|
||||
path: '/setup'
|
||||
fullPath: '/setup'
|
||||
preLoaderRoute: typeof SetupRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_auth/login': {
|
||||
id: '/_auth/login'
|
||||
path: '/login'
|
||||
fullPath: '/login'
|
||||
preLoaderRoute: typeof AuthLoginRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_app/upcoming': {
|
||||
id: '/_app/upcoming'
|
||||
path: '/upcoming'
|
||||
fullPath: '/upcoming'
|
||||
preLoaderRoute: typeof AppUpcomingRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_app/settings': {
|
||||
id: '/_app/settings'
|
||||
path: '/settings'
|
||||
fullPath: '/settings'
|
||||
preLoaderRoute: typeof AppSettingsRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_app/onboarding': {
|
||||
id: '/_app/onboarding'
|
||||
path: '/onboarding'
|
||||
fullPath: '/onboarding'
|
||||
preLoaderRoute: typeof AppOnboardingRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_app/library': {
|
||||
id: '/_app/library'
|
||||
path: '/library'
|
||||
fullPath: '/library'
|
||||
preLoaderRoute: typeof AppLibraryRouteImport
|
||||
'/_app/dashboard': {
|
||||
id: '/_app/dashboard'
|
||||
path: '/dashboard'
|
||||
fullPath: '/dashboard'
|
||||
preLoaderRoute: typeof AppDashboardRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_app/explore': {
|
||||
@@ -271,11 +236,53 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppExploreRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_app/dashboard': {
|
||||
id: '/_app/dashboard'
|
||||
path: '/dashboard'
|
||||
fullPath: '/dashboard'
|
||||
preLoaderRoute: typeof AppDashboardRouteImport
|
||||
'/_app/library': {
|
||||
id: '/_app/library'
|
||||
path: '/library'
|
||||
fullPath: '/library'
|
||||
preLoaderRoute: typeof AppLibraryRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_app/onboarding': {
|
||||
id: '/_app/onboarding'
|
||||
path: '/onboarding'
|
||||
fullPath: '/onboarding'
|
||||
preLoaderRoute: typeof AppOnboardingRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_app/settings': {
|
||||
id: '/_app/settings'
|
||||
path: '/settings'
|
||||
fullPath: '/settings'
|
||||
preLoaderRoute: typeof AppSettingsRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_app/upcoming': {
|
||||
id: '/_app/upcoming'
|
||||
path: '/upcoming'
|
||||
fullPath: '/upcoming'
|
||||
preLoaderRoute: typeof AppUpcomingRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_auth/login': {
|
||||
id: '/_auth/login'
|
||||
path: '/login'
|
||||
fullPath: '/login'
|
||||
preLoaderRoute: typeof AuthLoginRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/register': {
|
||||
id: '/_auth/register'
|
||||
path: '/register'
|
||||
fullPath: '/register'
|
||||
preLoaderRoute: typeof AuthRegisterRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_app/people/$id': {
|
||||
id: '/_app/people/$id'
|
||||
path: '/people/$id'
|
||||
fullPath: '/people/$id'
|
||||
preLoaderRoute: typeof AppPeopleIdRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_app/titles/$id': {
|
||||
@@ -285,13 +292,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppTitlesIdRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_app/people/$id': {
|
||||
id: '/_app/people/$id'
|
||||
path: '/people/$id'
|
||||
fullPath: '/people/$id'
|
||||
preLoaderRoute: typeof AppPeopleIdRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,12 @@ import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
import { TanStackDevtools } from "@tanstack/react-devtools";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools";
|
||||
import { createRootRouteWithContext, HeadContent, Outlet } from "@tanstack/react-router";
|
||||
import {
|
||||
createRootRouteWithContext,
|
||||
type ErrorComponentProps,
|
||||
HeadContent,
|
||||
Outlet,
|
||||
} from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||
import { Provider as StoreProvider } from "jotai";
|
||||
import { MotionConfig } from "motion/react";
|
||||
@@ -91,6 +96,8 @@ function AppShell() {
|
||||
}
|
||||
|
||||
function GlobalNotFound() {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-6">
|
||||
<div className="bg-primary/4 pointer-events-none absolute top-[40%] left-1/2 h-[500px] w-[500px] -translate-x-1/2 -translate-y-1/2 rounded-full blur-[160px]" />
|
||||
@@ -121,6 +128,7 @@ function GlobalNotFound() {
|
||||
>
|
||||
<a
|
||||
href="/"
|
||||
aria-label={t`Return home`}
|
||||
className="group bg-primary text-primary-foreground hover:shadow-primary/20 relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg px-6 text-sm font-medium transition-shadow hover:shadow-lg"
|
||||
>
|
||||
<span className="relative z-10">
|
||||
@@ -140,7 +148,9 @@ function GlobalNotFound() {
|
||||
);
|
||||
}
|
||||
|
||||
function GlobalError({ reset }: { error: Error; reset: () => void }) {
|
||||
function GlobalError({ reset }: ErrorComponentProps) {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-6 py-24 text-center">
|
||||
<div className="border-destructive/20 bg-destructive/5 text-destructive flex size-14 items-center justify-center rounded-full border">
|
||||
@@ -160,6 +170,7 @@ function GlobalError({ reset }: { error: Error; reset: () => void }) {
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t`Try again`}
|
||||
onClick={reset}
|
||||
className="group bg-primary text-primary-foreground hover:shadow-primary/20 relative inline-flex h-10 items-center justify-center gap-2 overflow-hidden rounded-lg px-5 text-sm font-medium transition-shadow hover:shadow-lg"
|
||||
>
|
||||
|
||||
+405
-327
File diff suppressed because it is too large.
Load diff
@@ -2,18 +2,26 @@
|
||||
title: Create or update integration
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Create a new media server integration or update an existing one.
|
||||
- content: Create a new media server integration or update an existing one.
|
||||
Generates a unique webhook token for the provider.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Create a new media server integration or update an existing one. Generates a unique webhook token for the provider.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/integrations","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/account/integrations","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
title: Delete integration
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: DELETE
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
@@ -12,6 +15,12 @@ _openapi:
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Remove a media server integration and all its event history.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/integrations/{provider}","method":"delete"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/account/integrations/{provider}","method":"delete"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: List integrations
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch all configured media server integrations for the current user,
|
||||
- content: Fetch all configured media server integrations for the current user,
|
||||
including recent webhook/sync events for each.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch all configured media server integrations for the current user, including recent webhook/sync events for each.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/integrations","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/account/integrations","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Regenerate webhook token
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Generate a new webhook token for an integration. The old token is
|
||||
- content: Generate a new webhook token for an integration. The old token is
|
||||
immediately invalidated.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Generate a new webhook token for an integration. The old token is immediately invalidated.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/integrations/{provider}/regenerate-token","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/account/integrations/{provider}/regenerate-token","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
title: Get user's streaming platforms
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
@@ -12,6 +15,12 @@ _openapi:
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch the current user's subscribed streaming platform IDs.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/platforms","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/account/platforms","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
title: Remove avatar
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: DELETE
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
@@ -12,6 +15,12 @@ _openapi:
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Delete the current user's profile avatar, reverting to the default.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/avatar","method":"delete"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/account/avatar","method":"delete"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
title: Update display name
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: PUT
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
@@ -12,6 +15,12 @@ _openapi:
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Change the current user's display name.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/name","method":"put"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/account/name","method":"put"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
title: Update streaming platforms
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: PUT
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
@@ -12,6 +15,12 @@ _openapi:
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Set the current user's subscribed streaming platforms.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/platforms","method":"put"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/account/platforms","method":"put"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Upload avatar
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Upload a new profile avatar image. Accepts JPEG, PNG, WebP, or GIF up
|
||||
- content: Upload a new profile avatar image. Accepts JPEG, PNG, WebP, or GIF up
|
||||
to 2 MB.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Upload a new profile avatar image. Accepts JPEG, PNG, WebP, or GIF up to 2 MB.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/account/avatar","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/account/avatar","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Create backup
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Create a new manual database backup. The backup is tagged as a manual
|
||||
- content: Create a new manual database backup. The backup is tagged as a manual
|
||||
backup.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Create a new manual database backup. The backup is tagged as a manual backup.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/backups","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/backups","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
title: Delete backup
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: DELETE
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
@@ -12,6 +15,12 @@ _openapi:
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Permanently delete a database backup file by filename.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/backups/{filename}","method":"delete"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/backups/{filename}","method":"delete"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
title: List backups
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
@@ -12,6 +15,12 @@ _openapi:
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch all database backups with their sizes and creation times.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/backups","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/backups","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Restore from backup
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Upload a database backup file and restore it. A pre-restore backup is
|
||||
- content: Upload a database backup file and restore it. A pre-restore backup is
|
||||
automatically created before overwriting the current database.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Upload a database backup file and restore it. A pre-restore backup is automatically created before overwriting the current database.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/backups/restore","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/backups/restore","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
title: Get backup schedule
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
@@ -12,6 +15,12 @@ _openapi:
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch the current automated backup schedule configuration.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/backups/schedule","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/backups/schedule","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Update backup schedule
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: PUT
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Update the automated backup schedule. Only provided fields are
|
||||
changed; omitted fields keep their current values.
|
||||
- content: Update the automated backup schedule. Only provided fields are changed;
|
||||
omitted fields keep their current values.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Update the automated backup schedule. Only provided fields are changed; omitted fields keep their current values.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/backups/schedule","method":"put"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/backups/schedule","method":"put"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Purge image cache
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Delete all cached TMDB images from disk. Images will be re-downloaded
|
||||
- content: Delete all cached TMDB images from disk. Images will be re-downloaded
|
||||
on demand.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Delete all cached TMDB images from disk. Images will be re-downloaded on demand.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/cache/purge-images","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/cache/purge-images","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Purge metadata cache
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Delete un-enriched stub titles not in any user's library and clean up
|
||||
- content: Delete un-enriched stub titles not in any user's library and clean up
|
||||
orphaned person records.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Delete un-enriched stub titles not in any user's library and clean up orphaned person records.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/cache/purge-metadata","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/cache/purge-metadata","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Get admin settings
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch all admin-configurable settings: registration, update checks,
|
||||
and telemetry.
|
||||
- content: 'Fetch all admin-configurable settings: registration, update checks,
|
||||
and telemetry.'
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch all admin-configurable settings: registration, update checks, and telemetry.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/settings","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/settings","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Update admin settings
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: PATCH
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Partially update admin settings. Only provided sections are changed;
|
||||
- content: Partially update admin settings. Only provided sections are changed;
|
||||
omitted sections keep their current values.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Partially update admin settings. Only provided sections are changed; omitted sections keep their current values.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/settings","method":"patch"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/settings","method":"patch"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Get system health report
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Comprehensive health check covering database, TMDB connectivity, cron
|
||||
- content: Comprehensive health check covering database, TMDB connectivity, cron
|
||||
jobs, image cache, backups, and environment.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Comprehensive health check covering database, TMDB connectivity, cron jobs, image cache, backups, and environment.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/system-health","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/system-health","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Trigger cron job
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Manually trigger a background cron job by name. The job runs
|
||||
- content: Manually trigger a background cron job by name. The job runs
|
||||
asynchronously.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Manually trigger a background cron job by name. The job runs asynchronously.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/admin/jobs/trigger","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/admin/jobs/trigger","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Browse titles with filters
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Browse movies or TV shows filtered by genre, sorted by popularity.
|
||||
- content: Browse movies or TV shows filtered by genre, sorted by popularity.
|
||||
Returns user statuses and episode progress.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Browse movies or TV shows filtered by genre, sorted by popularity. Returns user statuses and episode progress.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/discover/browse","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/discover/browse","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: List genres
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch the list of TMDB genres for movies or TV shows. Used to populate
|
||||
- content: Fetch the list of TMDB genres for movies or TV shows. Used to populate
|
||||
genre filters for discovery.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch the list of TMDB genres for movies or TV shows. Used to populate genre filters for discovery.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/discover/genres","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/discover/genres","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: List available streaming platforms
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch all available streaming platforms, ordered by popularity.
|
||||
- content: Fetch all available streaming platforms, ordered by popularity.
|
||||
Includes subscription status for filtering.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch all available streaming platforms, ordered by popularity. Includes subscription status for filtering.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/discover/platforms","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/discover/platforms","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Get popular titles
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch currently popular movies or TV shows from TMDB with the user's
|
||||
- content: Fetch currently popular movies or TV shows from TMDB with the user's
|
||||
tracking statuses.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch currently popular movies or TV shows from TMDB with the user's tracking statuses.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/discover/popular","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/discover/popular","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Get personalized recommendations
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch personalized title recommendations based on the user's library
|
||||
- content: Fetch personalized title recommendations based on the user's library
|
||||
and watch history.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch personalized title recommendations based on the user's library and watch history.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/discover/recommendations","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/discover/recommendations","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Search movies, TV shows, and people
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Full-text search across movies, TV shows, and people via TMDB.
|
||||
- content: Full-text search across movies, TV shows, and people via TMDB.
|
||||
Optionally filter by media type.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Full-text search across movies, TV shows, and people via TMDB. Optionally filter by media type.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/discover/search","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/discover/search","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Get trending titles
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch today's trending movies and/or TV shows from TMDB, including a
|
||||
- content: Fetch today's trending movies and/or TV shows from TMDB, including a
|
||||
featured hero title and the user's statuses.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch today's trending movies and/or TV shows from TMDB, including a featured hero title and the user's statuses.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/discover/trending","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/discover/trending","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Cancel import job
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Cancel a pending or running import job. Already-imported items are not
|
||||
- content: Cancel a pending or running import job. Already-imported items are not
|
||||
rolled back.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Cancel a pending or running import job. Already-imported items are not rolled back.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/jobs/{id}/cancel","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/imports/jobs/{id}/cancel","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
title: Create import job
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
@@ -12,6 +15,12 @@ _openapi:
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Create and start a background import job from previously parsed data.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/jobs","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/imports/jobs","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
title: Get import job status
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
@@ -12,6 +15,12 @@ _openapi:
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Get the current status and progress of an import job.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/jobs/{id}","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/imports/jobs/{id}","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Stream import job events
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
SSE stream of import job progress events. Yields progress updates and
|
||||
a final complete event.
|
||||
- content: SSE stream of import job progress events. Yields progress updates and a
|
||||
final complete event.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
SSE stream of import job progress events. Yields progress updates and a final complete event.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/jobs/{id}/events","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/imports/jobs/{id}/events","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Parse import file
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Upload and parse an export file from Trakt, Simkl, Letterboxd, or
|
||||
Sofa. Returns a preview of items found without importing anything.
|
||||
- content: Upload and parse an export file from Trakt, Simkl, Letterboxd, or Sofa.
|
||||
Returns a preview of items found without importing anything.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Upload and parse an export file from Trakt, Simkl, Letterboxd, or Sofa. Returns a preview of items found without importing anything.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/parse-file","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/imports/parse-file","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Parse raw OAuth import data
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Accept raw API responses from the OAuth proxy, parse them into
|
||||
- content: Accept raw API responses from the OAuth proxy, parse them into
|
||||
normalized import format, and return a preview with item counts.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Accept raw API responses from the OAuth proxy, parse them into normalized import format, and return a preview with item counts.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/imports/parse-payload","method":"post"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/imports/parse-payload","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Get continue watching list
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch TV shows the user is currently watching, with the next unwatched
|
||||
- content: Fetch TV shows the user is currently watching, with the next unwatched
|
||||
episode and progress for each.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch TV shows the user is currently watching, with the next unwatched episode and progress for each.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/library/continue-watching","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/library/continue-watching","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: List genres in user's library
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Get the distinct genres present in the user's library, ordered
|
||||
- content: Get the distinct genres present in the user's library, ordered
|
||||
alphabetically. Used to populate the genre filter dropdown.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Get the distinct genres present in the user's library, ordered alphabetically. Used to populate the genre filter dropdown.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/library/genres","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/library/genres","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,19 +2,27 @@
|
||||
title: List library with filters
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch paginated, filtered, and sorted titles from the user's library.
|
||||
- content: Fetch paginated, filtered, and sorted titles from the user's library.
|
||||
Supports filtering by status, type, genre, rating, year, content
|
||||
rating, and streaming availability.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch paginated, filtered, and sorted titles from the user's library. Supports filtering by status, type, genre, rating, year, content rating, and streaming availability.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/library","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/library","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
title: Get library statistics
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
@@ -12,6 +15,12 @@ _openapi:
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch aggregate library counts: total titles and completed titles.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/library/stats","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/library/stats","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Get upcoming episodes and movies
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch upcoming episodes and movie releases for titles in the user's
|
||||
- content: Fetch upcoming episodes and movie releases for titles in the user's
|
||||
library, sorted by date. Supports cursor-based pagination.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch upcoming episodes and movie releases for titles in the user's library, sorted by date. Supports cursor-based pagination.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/library/upcoming","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/library/upcoming","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Get person details
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch a person's profile and paginated filmography. Imports from TMDB
|
||||
- content: Fetch a person's profile and paginated filmography. Imports from TMDB
|
||||
on first access if not yet cached locally.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch a person's profile and paginated filmography. Imports from TMDB on first access if not yet cached locally.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/people/{id}","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/people/{id}","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Get public instance info
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch public information about this Sofa instance including
|
||||
- content: Fetch public information about this Sofa instance including
|
||||
authentication configuration. Does not require authentication.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch public information about this Sofa instance including authentication configuration. Does not require authentication.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/system/public-info","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/system/public-info","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Get internal system config
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Returns internal configuration such as the public API URL. Requires
|
||||
- content: Returns internal configuration such as the public API URL. Requires
|
||||
authentication.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Returns internal configuration such as the public API URL. Requires authentication.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/system/status","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/system/status","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,19 +2,27 @@
|
||||
title: Get title details
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch full title metadata including seasons, cast, and streaming
|
||||
- content: Fetch full title metadata including seasons, cast, and streaming
|
||||
availability. Imports from TMDB on first access if not yet cached
|
||||
locally.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch full title metadata including seasons, cast, and streaming availability. Imports from TMDB on first access if not yet cached locally.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/titles/{id}","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/titles/{id}","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,26 @@
|
||||
title: Get similar titles
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: GET
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fetch similar titles based on locally cached recommendation data,
|
||||
along with the user's statuses for each.
|
||||
- content: Fetch similar titles based on locally cached recommendation data, along
|
||||
with the user's statuses for each.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
Fetch similar titles based on locally cached recommendation data, along with the user's statuses for each.
|
||||
|
||||
<APIPage document={"./public/openapi.json"} operations={[{"path":"/titles/{id}/similar","method":"get"}]} />
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/titles/{id}/similar","method":"get"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user