mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-11-21 14:03:21 -05:00
Add settings page (#1143)
This commit is contained in:
commit
4f9d340ef4
@ -276,7 +276,7 @@ set
|
||||
username = coalesce($2, username),
|
||||
email = coalesce($3, email),
|
||||
password = coalesce($4, password),
|
||||
claims = coalesce($5, claims)
|
||||
claims = claims || coalesce($5, '{}'::jsonb)
|
||||
where
|
||||
id = $1
|
||||
returning
|
||||
|
||||
@ -10,6 +10,6 @@ pkgs.mkShell {
|
||||
postgresql_15
|
||||
pgformatter
|
||||
# to run tests
|
||||
# hurl
|
||||
hurl
|
||||
];
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ set
|
||||
username = coalesce(sqlc.narg(username), username),
|
||||
email = coalesce(sqlc.narg(email), email),
|
||||
password = coalesce(sqlc.narg(password), password),
|
||||
claims = coalesce(sqlc.narg(claims), claims)
|
||||
claims = claims || coalesce(sqlc.narg(claims), '{}'::jsonb)
|
||||
where
|
||||
id = $1
|
||||
returning
|
||||
|
||||
@ -26,7 +26,16 @@ jwt: jsonpath "$.token"
|
||||
PATCH {{host}}/users/me/password
|
||||
Authorization: Bearer {{jwt}}
|
||||
{
|
||||
"password": "new-password"
|
||||
"oldPassword": "invalid-one",
|
||||
"newPassword": "wont-be-changed"
|
||||
}
|
||||
HTTP 403
|
||||
|
||||
PATCH {{host}}/users/me/password
|
||||
Authorization: Bearer {{jwt}}
|
||||
{
|
||||
"oldPassword": "password-login-user",
|
||||
"newPassword": "new-password"
|
||||
}
|
||||
HTTP 204
|
||||
|
||||
|
||||
@ -59,7 +59,8 @@ type EditUserDto struct {
|
||||
}
|
||||
|
||||
type EditPasswordDto struct {
|
||||
Password string `json:"password" validate:"required" example:"password1234"`
|
||||
OldPassword string `json:"oldPassword" validate:"required" example:"password1234"`
|
||||
NewPassword string `json:"newPassword" validate:"required" example:"password1234"`
|
||||
}
|
||||
|
||||
func MapDbUser(user *dbc.User) User {
|
||||
@ -182,7 +183,7 @@ func (h *Handler) GetMe(c echo.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbuser, err := h.db.GetUser(context.Background(), dbc.GetUserParams{
|
||||
dbuser, err := h.db.GetUser(c.Request().Context(), dbc.GetUserParams{
|
||||
UseId: true,
|
||||
Id: id,
|
||||
})
|
||||
@ -406,6 +407,10 @@ func (h *Handler) ChangePassword(c echo.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := h.db.GetUser(c.Request().Context(), dbc.GetUserParams{
|
||||
UseId: true,
|
||||
Id: uid,
|
||||
})
|
||||
|
||||
sid, err := GetCurrentSessionId(c)
|
||||
if err != nil {
|
||||
@ -421,13 +426,26 @@ func (h *Handler) ChangePassword(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
match, err := argon2id.ComparePasswordAndHash(
|
||||
req.OldPassword,
|
||||
*user[0].User.Password,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !match {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Invalid password")
|
||||
}
|
||||
|
||||
pass, err := argon2id.CreateHash(req.NewPassword, argon2id.DefaultParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = h.db.UpdateUser(context.Background(), dbc.UpdateUserParams{
|
||||
Id: uid,
|
||||
Password: &req.Password,
|
||||
Password: &pass,
|
||||
})
|
||||
if err == pgx.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "Invalid token, user not found")
|
||||
} else if err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
FROM oven/bun AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# idk why it doesnt' build without it
|
||||
# https://github.com/oven-sh/bun/issues/24538
|
||||
# Remember to also remove `tsx` after this bug is fixed
|
||||
RUN apt update && apt install -y nodejs && rm /usr/local/bun-node-fallback-bin/node
|
||||
|
||||
ENV NODE_ENV=production
|
||||
COPY package.json bun.lock scripts .
|
||||
COPY package.json bun.lock .
|
||||
COPY scripts scripts
|
||||
COPY public public
|
||||
RUN bun install --production
|
||||
|
||||
COPY . .
|
||||
|
||||
@ -3,6 +3,7 @@ WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock .
|
||||
COPY scripts scripts
|
||||
COPY public public
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import "tsx/cjs";
|
||||
import type { ExpoConfig } from "expo/config";
|
||||
import { supportedLanguages } from "./src/providers/translations.compile.ts";
|
||||
|
||||
const IS_DEV = process.env.APP_VARIANT === "development";
|
||||
|
||||
@ -75,6 +77,12 @@ export const expo: ExpoConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"react-native-localization-settings",
|
||||
{
|
||||
languages: supportedLanguages,
|
||||
}
|
||||
]
|
||||
],
|
||||
experiments: {
|
||||
typedRoutes: true,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"extends": "//",
|
||||
"files": {
|
||||
"includes": ["src/**"]
|
||||
"includes": [
|
||||
"src/**",
|
||||
"scripts/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
"expo-splash-screen": "^31.0.10",
|
||||
"expo-status-bar": "~3.0.8",
|
||||
"expo-updates": "~29.0.11",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jassub": "^1.8.6",
|
||||
"langmap": "^0.0.16",
|
||||
@ -36,6 +37,7 @@
|
||||
"react-i18next": "^16.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-get-random-values": "^2.0.0",
|
||||
"react-native-localization-settings": "^1.2.0",
|
||||
"react-native-mmkv": "^3.3.3",
|
||||
"react-native-nitro-modules": "^0.30.2",
|
||||
"react-native-reanimated": "~4.1.2",
|
||||
@ -48,6 +50,7 @@
|
||||
"react-native-worklets": "0.5.1",
|
||||
"react-tooltip": "^5.29.1",
|
||||
"sweetalert2": "^11.26.3",
|
||||
"tsx": "^4.20.6",
|
||||
"uuid": "^13.0.0",
|
||||
"video.js": "^8.23.4",
|
||||
"yoshiki": "1.2.14",
|
||||
@ -271,6 +274,58 @@
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-yx0CqeOhPjYQ5ZXgPfu8QYkgBhVJyvWe36as7jRuPrKPO5ylVDfwVtPQ+K/mooNTADW0IhxOZm3aPu16dP8yNQ=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@expo/cli": ["@expo/cli@54.0.12", "", { "dependencies": { "@0no-co/graphql.web": "^1.0.8", "@expo/code-signing-certificates": "^0.0.5", "@expo/config": "~12.0.10", "@expo/config-plugins": "~54.0.2", "@expo/devcert": "^1.1.2", "@expo/env": "~2.0.7", "@expo/image-utils": "^0.8.7", "@expo/json-file": "^10.0.7", "@expo/mcp-tunnel": "~0.0.7", "@expo/metro": "~54.1.0", "@expo/metro-config": "~54.0.7", "@expo/osascript": "^2.3.7", "@expo/package-manager": "^1.9.8", "@expo/plist": "^0.4.7", "@expo/prebuild-config": "^54.0.5", "@expo/schema-utils": "^0.1.7", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", "@react-native/dev-middleware": "0.81.4", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", "expo-server": "^1.0.2", "freeport-async": "^2.0.0", "getenv": "^2.0.0", "glob": "^10.4.2", "lan-network": "^0.1.6", "minimatch": "^9.0.0", "node-forge": "^1.3.1", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^3.0.1", "pretty-bytes": "^5.6.0", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "qrcode-terminal": "0.11.0", "require-from-string": "^2.0.2", "requireg": "^0.2.2", "resolve": "^1.22.2", "resolve-from": "^5.0.0", "resolve.exports": "^2.0.3", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "tar": "^7.4.3", "terminal-link": "^2.1.1", "undici": "^6.18.2", "wrap-ansi": "^7.0.0", "ws": "^8.12.1" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-aBwpzG8z5U4b51S3T5MRIRe+NOOW2KdJ7cvJD8quL2Ba9gZRw8UVb+pmL28tS9yL3r1r3n8b1COSaJ8Y0eRTFA=="],
|
||||
|
||||
"@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.5", "", { "dependencies": { "node-forge": "^1.2.1", "nullthrows": "^1.1.1" } }, "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw=="],
|
||||
@ -781,6 +836,8 @@
|
||||
|
||||
"error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
@ -897,6 +954,8 @@
|
||||
|
||||
"get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||
|
||||
"getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
|
||||
|
||||
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
@ -927,6 +986,8 @@
|
||||
|
||||
"i18next": ["i18next@25.6.0", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw=="],
|
||||
|
||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
|
||||
|
||||
"i18next-http-backend": ["i18next-http-backend@3.0.2", "", { "dependencies": { "cross-fetch": "4.0.0" } }, "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
@ -1259,6 +1320,8 @@
|
||||
|
||||
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="],
|
||||
|
||||
"react-native-localization-settings": ["react-native-localization-settings@1.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-pxX/mfokqjwIdb1zINuN6DLL4PeVHTaIGz2Tk833tS94fmpsSuPoYnkCmtXsfvZjxhDOSsRceao/JutJbIlpIQ=="],
|
||||
|
||||
"react-native-mmkv": ["react-native-mmkv@3.3.3", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-GMsfOmNzx0p5+CtrCFRVtpOOMYNJXuksBVARSQrCFaZwjUyHJdQzcN900GGaFFNTxw2fs8s5Xje//RDKj9+PZA=="],
|
||||
|
||||
"react-native-nitro-modules": ["react-native-nitro-modules@0.30.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+/uVS7FQwOiKYZQERMIvBRv5/X3CVHrFG6Nr/kIhVfVxGeUimHnBz7cgA97lJKIn7AKDRWL+UjLedW8pGOt0dg=="],
|
||||
@ -1313,6 +1376,8 @@
|
||||
|
||||
"resolve-global": ["resolve-global@1.0.0", "", { "dependencies": { "global-dirs": "^0.1.1" } }, "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"resolve-workspace-root": ["resolve-workspace-root@2.0.0", "", {}, "sha512-IsaBUZETJD5WsI11Wt8PKHwaIe45or6pwNc8yflvLJ4DWtImK9kuLoH5kUva/2Mmx/RdIyr4aONNSa2v9LTJsw=="],
|
||||
|
||||
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
|
||||
@ -1447,6 +1512,8 @@
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="],
|
||||
|
||||
"type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="],
|
||||
|
||||
"type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
"expo-splash-screen": "^31.0.10",
|
||||
"expo-status-bar": "~3.0.8",
|
||||
"expo-updates": "~29.0.11",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jassub": "^1.8.6",
|
||||
"langmap": "^0.0.16",
|
||||
@ -47,6 +48,7 @@
|
||||
"react-i18next": "^16.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-get-random-values": "^2.0.0",
|
||||
"react-native-localization-settings": "^1.2.0",
|
||||
"react-native-mmkv": "^3.3.3",
|
||||
"react-native-nitro-modules": "^0.30.2",
|
||||
"react-native-reanimated": "~4.1.2",
|
||||
@ -59,6 +61,7 @@
|
||||
"react-native-worklets": "0.5.1",
|
||||
"react-tooltip": "^5.29.1",
|
||||
"sweetalert2": "^11.26.3",
|
||||
"tsx": "^4.20.6",
|
||||
"uuid": "^13.0.0",
|
||||
"video.js": "^8.23.4",
|
||||
"yoshiki": "1.2.14",
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { deleteData, setUserTheme, storeData, useUserTheme } from "@kyoo/models";
|
||||
import { Link, Select } from "@kyoo/primitives";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Preference, SettingsContainer } from "./base";
|
||||
|
||||
import Theme from "@material-symbols/svg-400/outlined/dark_mode.svg";
|
||||
import Language from "@material-symbols/svg-400/outlined/language.svg";
|
||||
|
||||
import Android from "@material-symbols/svg-400/rounded/android.svg";
|
||||
import Public from "@material-symbols/svg-400/rounded/public.svg";
|
||||
import { useLanguageName } from "../utils";
|
||||
|
||||
export const GeneralSettings = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const theme = useUserTheme("auto");
|
||||
const getLanguageName = useLanguageName();
|
||||
|
||||
const changeLanguage = (lang: string) => {
|
||||
if (lang === "system") {
|
||||
i18n.changeLanguage(i18n.systemLanguage);
|
||||
deleteData("language");
|
||||
return;
|
||||
}
|
||||
storeData("language", lang);
|
||||
i18n.changeLanguage(lang);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.general.label")}>
|
||||
<Preference
|
||||
icon={Theme}
|
||||
label={t("settings.general.theme.label")}
|
||||
description={t("settings.general.theme.description")}
|
||||
>
|
||||
<Select
|
||||
label={t("settings.general.theme.label")}
|
||||
value={theme}
|
||||
onValueChange={(value) => setUserTheme(value)}
|
||||
values={["auto", "light", "dark"]}
|
||||
getLabel={(key) => t(`settings.general.theme.${key}`)}
|
||||
/>
|
||||
</Preference>
|
||||
<Preference
|
||||
icon={Language}
|
||||
label={t("settings.general.language.label")}
|
||||
description={t("settings.general.language.description")}
|
||||
>
|
||||
<Select
|
||||
label={t("settings.general.language.label")}
|
||||
value={i18n.resolvedLanguage! === i18n.systemLanguage ? "system" : i18n.resolvedLanguage!}
|
||||
onValueChange={(value) => changeLanguage(value)}
|
||||
values={["system", ...Object.keys(i18n.options.resources!)]}
|
||||
getLabel={(key) =>
|
||||
key === "system" ? t("settings.general.language.system") : (getLanguageName(key) ?? key)
|
||||
}
|
||||
/>
|
||||
</Preference>
|
||||
</SettingsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const About = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.about.label")}>
|
||||
<Link href="https://github.com/zoriya/kyoo/releases/latest/download/kyoo.apk" target="_blank">
|
||||
<Preference
|
||||
icon={Android}
|
||||
label={t("settings.about.android-app.label")}
|
||||
description={t("settings.about.android-app.description")}
|
||||
/>
|
||||
</Link>
|
||||
<Link href="https://github.com/zoriya/kyoo" target="_blank">
|
||||
<Preference
|
||||
icon={Public}
|
||||
label={t("settings.about.git.label")}
|
||||
description={t("settings.about.git.description")}
|
||||
/>
|
||||
</Link>
|
||||
</SettingsContainer>
|
||||
);
|
||||
};
|
||||
@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { type QueryPage, useAccount } from "@kyoo/models";
|
||||
import { ts } from "@kyoo/primitives";
|
||||
import { ScrollView } from "react-native";
|
||||
import { DefaultLayout } from "../layout";
|
||||
import { AccountSettings } from "./account";
|
||||
import { About, GeneralSettings } from "./general";
|
||||
import { OidcSettings } from "./oidc";
|
||||
import { PlaybackSettings } from "./playback";
|
||||
|
||||
export const SettingsPage: QueryPage = () => {
|
||||
const account = useAccount();
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ gap: ts(4), paddingBottom: ts(4) }}>
|
||||
<GeneralSettings />
|
||||
{account && <PlaybackSettings />}
|
||||
{account && <AccountSettings />}
|
||||
{account && <OidcSettings />}
|
||||
<About />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsPage.getLayout = DefaultLayout;
|
||||
SettingsPage.getFetchUrls = () => [OidcSettings.query()];
|
||||
@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
type QueryIdentifier,
|
||||
type ServerInfo,
|
||||
ServerInfoP,
|
||||
queryFn,
|
||||
useAccount,
|
||||
useFetch,
|
||||
} from "@kyoo/models";
|
||||
import { Button, IconButton, Link, Skeleton, tooltip, ts } from "@kyoo/primitives";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ImageBackground } from "react-native";
|
||||
import { rem, useYoshiki } from "yoshiki/native";
|
||||
import { ErrorView } from "../../../../src/ui/errors";
|
||||
import { Preference, SettingsContainer } from "./base";
|
||||
|
||||
import Badge from "@material-symbols/svg-400/outlined/badge.svg";
|
||||
import Remove from "@material-symbols/svg-400/outlined/close.svg";
|
||||
import OpenProfile from "@material-symbols/svg-400/outlined/open_in_new.svg";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export const OidcSettings = () => {
|
||||
const account = useAccount()!;
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const { data, error } = useFetch(OidcSettings.query());
|
||||
const queryClient = useQueryClient();
|
||||
const { mutateAsync: unlinkAccount } = useMutation({
|
||||
mutationFn: async (provider: string) =>
|
||||
await queryFn({
|
||||
path: ["auth", "login", provider],
|
||||
method: "DELETE",
|
||||
}),
|
||||
onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["auth", "me"] }),
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.oidc.label")}>
|
||||
{error ? (
|
||||
<ErrorView error={error} />
|
||||
) : data ? (
|
||||
Object.entries(data.oidc).map(([id, x]) => {
|
||||
const acc = account.externalId[id];
|
||||
return (
|
||||
<Preference
|
||||
key={x.displayName}
|
||||
icon={Badge}
|
||||
label={x.displayName}
|
||||
description={
|
||||
acc
|
||||
? t("settings.oidc.connected", { username: acc.username })
|
||||
: t("settings.oidc.not-connected")
|
||||
}
|
||||
customIcon={
|
||||
x.logoUrl != null && (
|
||||
<ImageBackground
|
||||
source={{ uri: x.logoUrl }}
|
||||
{...css({ width: ts(3), height: ts(3), marginRight: ts(2) })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{acc ? (
|
||||
<>
|
||||
{acc.profileUrl && (
|
||||
<IconButton
|
||||
icon={OpenProfile}
|
||||
as={Link}
|
||||
href={acc.profileUrl}
|
||||
target="_blank"
|
||||
{...tooltip(t("settings.oidc.open-profile", { provider: x.displayName }))}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={Remove}
|
||||
onPress={() => unlinkAccount(id)}
|
||||
{...tooltip(t("settings.oidc.delete", { provider: x.displayName }))}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
text={t("settings.oidc.link")}
|
||||
as={Link}
|
||||
href={x.link}
|
||||
{...css({ minWidth: rem(6) })}
|
||||
/>
|
||||
)}
|
||||
</Preference>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
[...Array(3)].map((_, i) => (
|
||||
<Preference
|
||||
key={i}
|
||||
customIcon={<Skeleton {...css({ width: ts(3), height: ts(3) })} />}
|
||||
icon={null!}
|
||||
label={<Skeleton {...css({ width: rem(6) })} />}
|
||||
description={<Skeleton {...css({ width: rem(7), height: rem(0.8) })} />}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</SettingsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
OidcSettings.query = (): QueryIdentifier<ServerInfo> => ({
|
||||
path: ["info"],
|
||||
parser: ServerInfoP,
|
||||
});
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "ያጫውቱ",
|
||||
"trailer": "ትሬለሩን አጫውት",
|
||||
"studio": "ስቱዲዮ",
|
||||
"studios": "ስቱዲዮ",
|
||||
"genre": "ዘውግ",
|
||||
"genre-none": "ዘውግ የለውም",
|
||||
"staff": "ስታፍ",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": ""
|
||||
},
|
||||
"genres": {
|
||||
"Action": "",
|
||||
"Adventure": "",
|
||||
"Animation": "",
|
||||
"Comedy": "",
|
||||
"Crime": "",
|
||||
"Documentary": "",
|
||||
"Drama": "",
|
||||
"Family": "",
|
||||
"Fantasy": "",
|
||||
"History": "",
|
||||
"Horror": "",
|
||||
"Music": "",
|
||||
"Mystery": "",
|
||||
"Romance": "",
|
||||
"ScienceFiction": "",
|
||||
"Thriller": "",
|
||||
"War": "",
|
||||
"Western": "",
|
||||
"Kids": "",
|
||||
"News": "",
|
||||
"Reality": "",
|
||||
"Soap": "",
|
||||
"Talk": "",
|
||||
"Politics": ""
|
||||
"action": "",
|
||||
"adventure": "",
|
||||
"animation": "",
|
||||
"comedy": "",
|
||||
"crime": "",
|
||||
"documentary": "",
|
||||
"drama": "",
|
||||
"family": "",
|
||||
"fantasy": "",
|
||||
"history": "",
|
||||
"horror": "",
|
||||
"music": "",
|
||||
"mystery": "",
|
||||
"romance": "",
|
||||
"science-fiction": "",
|
||||
"thriller": "",
|
||||
"war": "",
|
||||
"western": "",
|
||||
"kids": "",
|
||||
"reality": "",
|
||||
"soap": "",
|
||||
"talk": "",
|
||||
"politics": ""
|
||||
},
|
||||
"misc": {
|
||||
"settings": "",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "تشغيل",
|
||||
"trailer": "تشغيل المقطع الدعائي",
|
||||
"studio": "استوديو",
|
||||
"studios": "استوديو",
|
||||
"genre": "الأنواع",
|
||||
"genre-none": "لا توجد أنواع",
|
||||
"staff": "الطاقم",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": "التبديل إلى عرض القائمة"
|
||||
},
|
||||
"genres": {
|
||||
"Action": "أكشن",
|
||||
"Adventure": "مغامرات",
|
||||
"Animation": "أنميشن",
|
||||
"Comedy": "كوميدي",
|
||||
"Crime": "جريمة",
|
||||
"Documentary": "وثائقي",
|
||||
"Drama": "دراما",
|
||||
"Family": "عائلي",
|
||||
"Fantasy": "خيال",
|
||||
"History": "تاريخي",
|
||||
"Horror": "رعب",
|
||||
"Music": "موسيقى",
|
||||
"Mystery": "غموض",
|
||||
"Romance": "رومانسي",
|
||||
"ScienceFiction": "خيال علمي",
|
||||
"Thriller": "إثارة",
|
||||
"War": "حرب",
|
||||
"Western": "غربي",
|
||||
"Kids": "أطفال",
|
||||
"News": "أخبار",
|
||||
"Reality": "واقع",
|
||||
"Soap": "دراما طويلة",
|
||||
"Talk": "حوار",
|
||||
"Politics": "سياسة"
|
||||
"action": "أكشن",
|
||||
"adventure": "مغامرات",
|
||||
"animation": "أنميشن",
|
||||
"comedy": "كوميدي",
|
||||
"crime": "جريمة",
|
||||
"documentary": "وثائقي",
|
||||
"drama": "دراما",
|
||||
"family": "عائلي",
|
||||
"fantasy": "خيال",
|
||||
"history": "تاريخي",
|
||||
"horror": "رعب",
|
||||
"music": "موسيقى",
|
||||
"mystery": "غموض",
|
||||
"romance": "رومانسي",
|
||||
"science-fiction": "خيال علمي",
|
||||
"thriller": "إثارة",
|
||||
"war": "حرب",
|
||||
"western": "غربي",
|
||||
"kids": "أطفال",
|
||||
"reality": "واقع",
|
||||
"soap": "دراما طويلة",
|
||||
"talk": "حوار",
|
||||
"politics": "سياسة"
|
||||
},
|
||||
"misc": {
|
||||
"settings": "إعدادات",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Abspielen",
|
||||
"trailer": "Trailer abspielen",
|
||||
"studio": "Studio",
|
||||
"studios": "Studio",
|
||||
"genre": "Genres",
|
||||
"genre-none": "Keine Genres",
|
||||
"staff": "Besetzung",
|
||||
@ -259,29 +259,28 @@
|
||||
}
|
||||
},
|
||||
"genres": {
|
||||
"Family": "Familienfilm",
|
||||
"Animation": "Animation",
|
||||
"Comedy": "Komödie",
|
||||
"Crime": "Krimi",
|
||||
"Documentary": "Dokumentation",
|
||||
"Drama": "Drama",
|
||||
"Fantasy": "Fantasy",
|
||||
"Horror": "Horror",
|
||||
"Mystery": "Mystery",
|
||||
"Romance": "Liebesfilm",
|
||||
"ScienceFiction": "Science-Fiction",
|
||||
"Thriller": "Thriller",
|
||||
"War": "Kriegsfilm",
|
||||
"Western": "Western",
|
||||
"Kids": "Kinderfilm",
|
||||
"News": "Neu",
|
||||
"Reality": "Reality-TV",
|
||||
"Soap": "Soap",
|
||||
"Talk": "Talkshow",
|
||||
"Politics": "Politik",
|
||||
"Adventure": "Abenteuer",
|
||||
"History": "Geschichte",
|
||||
"Music": "Musikfilm",
|
||||
"Action": "Action"
|
||||
"family": "Familienfilm",
|
||||
"animation": "Animation",
|
||||
"comedy": "Komödie",
|
||||
"crime": "Krimi",
|
||||
"documentary": "Dokumentation",
|
||||
"drama": "Drama",
|
||||
"fantasy": "Fantasy",
|
||||
"horror": "Horror",
|
||||
"mystery": "Mystery",
|
||||
"romance": "Liebesfilm",
|
||||
"science-fiction": "Science-Fiction",
|
||||
"thriller": "Thriller",
|
||||
"war": "Kriegsfilm",
|
||||
"western": "Western",
|
||||
"kids": "Kinderfilm",
|
||||
"reality": "Reality-TV",
|
||||
"soap": "Soap",
|
||||
"talk": "Talkshow",
|
||||
"politics": "Politik",
|
||||
"adventure": "Abenteuer",
|
||||
"history": "Geschichte",
|
||||
"music": "Musikfilm",
|
||||
"action": "Action"
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Play",
|
||||
"trailer": "Play Trailer",
|
||||
"studio": "Studio",
|
||||
"studios": "Studios",
|
||||
"genre": "Genres",
|
||||
"genre-none": "No genres",
|
||||
"staff": "Staff",
|
||||
@ -71,30 +71,29 @@
|
||||
"switchToList": "Switch to list view"
|
||||
},
|
||||
"genres": {
|
||||
"Action": "Action",
|
||||
"Adventure": "Adventure",
|
||||
"Animation": "Animation",
|
||||
"Comedy": "Comedy",
|
||||
"Crime": "Crime",
|
||||
"Documentary": "Documentary",
|
||||
"Drama": "Drama",
|
||||
"Family": "Family",
|
||||
"Fantasy": "Fantasy",
|
||||
"History": "History",
|
||||
"Horror": "Horror",
|
||||
"Music": "Music",
|
||||
"Mystery": "Mystery",
|
||||
"Romance": "Romance",
|
||||
"ScienceFiction": "Science Fiction",
|
||||
"Thriller": "Thriller",
|
||||
"War": "War",
|
||||
"Western": "Western",
|
||||
"Kids": "Kids",
|
||||
"News": "News",
|
||||
"Reality": "Reality",
|
||||
"Soap": "Soap",
|
||||
"Talk": "Talk",
|
||||
"Politics": "Politics"
|
||||
"action": "Action",
|
||||
"adventure": "Adventure",
|
||||
"animation": "Animation",
|
||||
"comedy": "Comedy",
|
||||
"crime": "Crime",
|
||||
"documentary": "Documentary",
|
||||
"drama": "Drama",
|
||||
"family": "Family",
|
||||
"fantasy": "Fantasy",
|
||||
"history": "History",
|
||||
"horror": "Horror",
|
||||
"music": "Music",
|
||||
"mystery": "Mystery",
|
||||
"romance": "Romance",
|
||||
"science-fiction": "Science Fiction",
|
||||
"thriller": "Thriller",
|
||||
"war": "War",
|
||||
"western": "Western",
|
||||
"kids": "Kids",
|
||||
"reality": "Reality",
|
||||
"politics": "Politics",
|
||||
"soap": "Soap",
|
||||
"talk": "Talk"
|
||||
},
|
||||
"misc": {
|
||||
"settings": "Settings",
|
||||
@ -129,8 +128,7 @@
|
||||
},
|
||||
"language": {
|
||||
"label": "Language",
|
||||
"description": "Set the language of your application",
|
||||
"system": "System"
|
||||
"description": "Set the language of your application"
|
||||
}
|
||||
},
|
||||
"playback": {
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Reproducir",
|
||||
"trailer": "Ver el tráiler",
|
||||
"studio": "Estudio",
|
||||
"studios": "Estudio",
|
||||
"genre": "Géneros",
|
||||
"genre-none": "Sin géneros",
|
||||
"staff": "Equipo",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": "Cambiar a vista de lista"
|
||||
},
|
||||
"genres": {
|
||||
"Action": "Acción",
|
||||
"Adventure": "Aventura",
|
||||
"Animation": "Animación",
|
||||
"Comedy": "Comedia",
|
||||
"Crime": "Crimen",
|
||||
"Documentary": "Documental",
|
||||
"Drama": "Drama",
|
||||
"Family": "Familia",
|
||||
"Fantasy": "Fantasía",
|
||||
"History": "Historia",
|
||||
"Horror": "Horror",
|
||||
"Music": "Musica",
|
||||
"Mystery": "Misterio",
|
||||
"Romance": "Romance",
|
||||
"ScienceFiction": "Ciencia ficción",
|
||||
"Thriller": "Suspenso",
|
||||
"War": "Bélica",
|
||||
"Western": "Del oeste",
|
||||
"Kids": "Niños",
|
||||
"News": "Noticias",
|
||||
"Reality": "Realidad",
|
||||
"Soap": "Novela",
|
||||
"Talk": "Entrevista",
|
||||
"Politics": "Política"
|
||||
"action": "Acción",
|
||||
"adventure": "Aventura",
|
||||
"animation": "Animación",
|
||||
"comedy": "Comedia",
|
||||
"crime": "Crimen",
|
||||
"documentary": "Documental",
|
||||
"drama": "Drama",
|
||||
"family": "Familia",
|
||||
"fantasy": "Fantasía",
|
||||
"history": "Historia",
|
||||
"horror": "Horror",
|
||||
"music": "Musica",
|
||||
"mystery": "Misterio",
|
||||
"romance": "Romance",
|
||||
"science-fiction": "Ciencia ficción",
|
||||
"thriller": "Suspenso",
|
||||
"war": "Bélica",
|
||||
"western": "Del oeste",
|
||||
"kids": "Niños",
|
||||
"reality": "Realidad",
|
||||
"soap": "Novela",
|
||||
"talk": "Entrevista",
|
||||
"politics": "Política"
|
||||
},
|
||||
"misc": {
|
||||
"settings": "Ajustes",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Lecture",
|
||||
"trailer": "Lire la bande annonce",
|
||||
"studio": "Studio",
|
||||
"studios": "Studio",
|
||||
"genre": "Genres",
|
||||
"genre-none": "Aucun genres",
|
||||
"staff": "Équipe",
|
||||
@ -259,29 +259,28 @@
|
||||
}
|
||||
},
|
||||
"genres": {
|
||||
"Action": "Action",
|
||||
"Adventure": "Aventure",
|
||||
"Comedy": "Comédie",
|
||||
"Documentary": "Documentaire",
|
||||
"Drama": "Drame",
|
||||
"Family": "Famille",
|
||||
"Fantasy": "Fantastique",
|
||||
"History": "Histoire",
|
||||
"Crime": "Scène de crime",
|
||||
"Horror": "Horreur",
|
||||
"Music": "Musique",
|
||||
"Mystery": "Mystère",
|
||||
"Romance": "Romance",
|
||||
"ScienceFiction": "Science-fiction",
|
||||
"War": "Guerre",
|
||||
"Kids": "Jeunesse",
|
||||
"Thriller": "Thriller",
|
||||
"Western": "Western",
|
||||
"Politics": "Politique",
|
||||
"Soap": "Soap",
|
||||
"Talk": "Talkshow",
|
||||
"Animation": "Animation",
|
||||
"News": "Nouveautés",
|
||||
"Reality": "Télé-réalité"
|
||||
"action": "Action",
|
||||
"adventure": "Aventure",
|
||||
"comedy": "Comédie",
|
||||
"documentary": "Documentaire",
|
||||
"drama": "Drame",
|
||||
"family": "Famille",
|
||||
"fantasy": "Fantastique",
|
||||
"history": "Histoire",
|
||||
"crime": "Scène de crime",
|
||||
"horror": "Horreur",
|
||||
"music": "Musique",
|
||||
"mystery": "Mystère",
|
||||
"romance": "Romance",
|
||||
"science-fiction": "Science-fiction",
|
||||
"war": "Guerre",
|
||||
"kids": "Jeunesse",
|
||||
"thriller": "Thriller",
|
||||
"western": "Western",
|
||||
"politics": "Politique",
|
||||
"soap": "Soap",
|
||||
"talk": "Talkshow",
|
||||
"animation": "Animation",
|
||||
"reality": "Télé-réalité"
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Reproducir",
|
||||
"trailer": "Reproducir tráiler",
|
||||
"studio": "Estudio",
|
||||
"studios": "Estudio",
|
||||
"genre": "Xéneros",
|
||||
"genre-none": "Sen xéneros",
|
||||
"staff": "Persoal",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": ""
|
||||
},
|
||||
"genres": {
|
||||
"Action": "",
|
||||
"Adventure": "",
|
||||
"Animation": "",
|
||||
"Comedy": "",
|
||||
"Crime": "",
|
||||
"Documentary": "",
|
||||
"Drama": "",
|
||||
"Family": "",
|
||||
"Fantasy": "",
|
||||
"History": "",
|
||||
"Horror": "",
|
||||
"Music": "",
|
||||
"Mystery": "",
|
||||
"Romance": "",
|
||||
"ScienceFiction": "",
|
||||
"Thriller": "",
|
||||
"War": "",
|
||||
"Western": "",
|
||||
"Kids": "",
|
||||
"News": "",
|
||||
"Reality": "",
|
||||
"Soap": "",
|
||||
"Talk": "",
|
||||
"Politics": ""
|
||||
"action": "",
|
||||
"adventure": "",
|
||||
"animation": "",
|
||||
"comedy": "",
|
||||
"crime": "",
|
||||
"documentary": "",
|
||||
"drama": "",
|
||||
"family": "",
|
||||
"fantasy": "",
|
||||
"history": "",
|
||||
"horror": "",
|
||||
"music": "",
|
||||
"mystery": "",
|
||||
"romance": "",
|
||||
"science-fiction": "",
|
||||
"thriller": "",
|
||||
"war": "",
|
||||
"western": "",
|
||||
"kids": "",
|
||||
"reality": "",
|
||||
"soap": "",
|
||||
"talk": "",
|
||||
"politics": ""
|
||||
},
|
||||
"misc": {
|
||||
"settings": "",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "",
|
||||
"trailer": "",
|
||||
"studio": "",
|
||||
"studios": "",
|
||||
"genre": "",
|
||||
"genre-none": "",
|
||||
"staff": "",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": ""
|
||||
},
|
||||
"genres": {
|
||||
"Action": "",
|
||||
"Adventure": "",
|
||||
"Animation": "",
|
||||
"Comedy": "",
|
||||
"Crime": "",
|
||||
"Documentary": "",
|
||||
"Drama": "",
|
||||
"Family": "",
|
||||
"Fantasy": "",
|
||||
"History": "",
|
||||
"Horror": "",
|
||||
"Music": "",
|
||||
"Mystery": "",
|
||||
"Romance": "",
|
||||
"ScienceFiction": "",
|
||||
"Thriller": "",
|
||||
"War": "",
|
||||
"Western": "",
|
||||
"Kids": "",
|
||||
"News": "",
|
||||
"Reality": "",
|
||||
"Soap": "",
|
||||
"Talk": "",
|
||||
"Politics": ""
|
||||
"action": "",
|
||||
"adventure": "",
|
||||
"animation": "",
|
||||
"comedy": "",
|
||||
"crime": "",
|
||||
"documentary": "",
|
||||
"drama": "",
|
||||
"family": "",
|
||||
"fantasy": "",
|
||||
"history": "",
|
||||
"horror": "",
|
||||
"music": "",
|
||||
"mystery": "",
|
||||
"romance": "",
|
||||
"science-fiction": "",
|
||||
"thriller": "",
|
||||
"war": "",
|
||||
"western": "",
|
||||
"kids": "",
|
||||
"reality": "",
|
||||
"soap": "",
|
||||
"talk": "",
|
||||
"politics": ""
|
||||
},
|
||||
"misc": {
|
||||
"settings": "",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Riproduci",
|
||||
"trailer": "Riproduci Trailer",
|
||||
"studio": "Studio",
|
||||
"studios": "Studio",
|
||||
"genre": "Generi",
|
||||
"genre-none": "Nessun genere",
|
||||
"staff": "Staff",
|
||||
@ -252,29 +252,28 @@
|
||||
}
|
||||
},
|
||||
"genres": {
|
||||
"Mystery": "Mistero",
|
||||
"Kids": "Bambini",
|
||||
"Western": "Western",
|
||||
"History": "Storico",
|
||||
"Romance": "Romantico",
|
||||
"ScienceFiction": "Fantascienza",
|
||||
"Thriller": "Thriller",
|
||||
"War": "Guerra",
|
||||
"Animation": "Animazione",
|
||||
"Action": "Azione",
|
||||
"Adventure": "Avventura",
|
||||
"Comedy": "Commedia",
|
||||
"Crime": "Criminale",
|
||||
"Documentary": "Documentario",
|
||||
"Drama": "Drammatico",
|
||||
"Family": "Per famiglie",
|
||||
"Horror": "Orrore",
|
||||
"Music": "Musica",
|
||||
"News": "Notizie",
|
||||
"Reality": "Reality",
|
||||
"Soap": "Telenovela",
|
||||
"Talk": "Talk Show",
|
||||
"Politics": "Politica",
|
||||
"Fantasy": "Fantasia"
|
||||
"mystery": "Mistero",
|
||||
"kids": "Bambini",
|
||||
"western": "Western",
|
||||
"history": "Storico",
|
||||
"romance": "Romantico",
|
||||
"science-fiction": "Fantascienza",
|
||||
"thriller": "Thriller",
|
||||
"war": "Guerra",
|
||||
"animation": "Animazione",
|
||||
"action": "Azione",
|
||||
"adventure": "Avventura",
|
||||
"comedy": "Commedia",
|
||||
"crime": "Criminale",
|
||||
"documentary": "Documentario",
|
||||
"drama": "Drammatico",
|
||||
"family": "Per famiglie",
|
||||
"horror": "Orrore",
|
||||
"music": "Musica",
|
||||
"reality": "Reality",
|
||||
"soap": "Telenovela",
|
||||
"talk": "Talk Show",
|
||||
"politics": "Politica",
|
||||
"fantasy": "Fantasia"
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "재생",
|
||||
"trailer": "예고편 재생",
|
||||
"studio": "스튜디오",
|
||||
"studios": "스튜디오",
|
||||
"genre": "장르",
|
||||
"genre-none": "장르 없음",
|
||||
"staff": "스태프",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": "리스트 뷰로 전환하기"
|
||||
},
|
||||
"genres": {
|
||||
"Action": "액션",
|
||||
"Adventure": "모험",
|
||||
"Animation": "애니메이션",
|
||||
"Comedy": "코미디",
|
||||
"Crime": "범죄",
|
||||
"Documentary": "다큐멘터리",
|
||||
"Drama": "드라마",
|
||||
"Family": "가족",
|
||||
"Fantasy": "판타지",
|
||||
"History": "역사",
|
||||
"Horror": "호러",
|
||||
"Music": "음악",
|
||||
"Mystery": "미스터리",
|
||||
"Romance": "로맨스",
|
||||
"ScienceFiction": "SF",
|
||||
"Thriller": "스릴러",
|
||||
"War": "전쟁",
|
||||
"Western": "서부",
|
||||
"Kids": "키즈",
|
||||
"News": "뉴스",
|
||||
"Reality": "리얼리티",
|
||||
"Soap": "신파",
|
||||
"Talk": "토크",
|
||||
"Politics": "정치"
|
||||
"action": "액션",
|
||||
"adventure": "모험",
|
||||
"animation": "애니메이션",
|
||||
"comedy": "코미디",
|
||||
"crime": "범죄",
|
||||
"documentary": "다큐멘터리",
|
||||
"drama": "드라마",
|
||||
"family": "가족",
|
||||
"fantasy": "판타지",
|
||||
"history": "역사",
|
||||
"horror": "호러",
|
||||
"music": "음악",
|
||||
"mystery": "미스터리",
|
||||
"romance": "로맨스",
|
||||
"science-fiction": "SF",
|
||||
"thriller": "스릴러",
|
||||
"war": "전쟁",
|
||||
"western": "서부",
|
||||
"kids": "키즈",
|
||||
"reality": "리얼리티",
|
||||
"soap": "신파",
|
||||
"talk": "토크",
|
||||
"politics": "정치"
|
||||
},
|
||||
"misc": {
|
||||
"settings": "설정",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "",
|
||||
"trailer": "",
|
||||
"studio": "",
|
||||
"studios": "",
|
||||
"genre": "",
|
||||
"genre-none": "",
|
||||
"staff": "",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": ""
|
||||
},
|
||||
"genres": {
|
||||
"Action": "",
|
||||
"Adventure": "",
|
||||
"Animation": "",
|
||||
"Comedy": "",
|
||||
"Crime": "",
|
||||
"Documentary": "",
|
||||
"Drama": "",
|
||||
"Family": "",
|
||||
"Fantasy": "",
|
||||
"History": "",
|
||||
"Horror": "",
|
||||
"Music": "",
|
||||
"Mystery": "",
|
||||
"Romance": "",
|
||||
"ScienceFiction": "",
|
||||
"Thriller": "",
|
||||
"War": "",
|
||||
"Western": "",
|
||||
"Kids": "",
|
||||
"News": "",
|
||||
"Reality": "",
|
||||
"Soap": "",
|
||||
"Talk": "",
|
||||
"Politics": ""
|
||||
"action": "",
|
||||
"adventure": "",
|
||||
"animation": "",
|
||||
"comedy": "",
|
||||
"crime": "",
|
||||
"documentary": "",
|
||||
"drama": "",
|
||||
"family": "",
|
||||
"fantasy": "",
|
||||
"history": "",
|
||||
"horror": "",
|
||||
"music": "",
|
||||
"mystery": "",
|
||||
"romance": "",
|
||||
"science-fiction": "",
|
||||
"thriller": "",
|
||||
"war": "",
|
||||
"western": "",
|
||||
"kids": "",
|
||||
"reality": "",
|
||||
"soap": "",
|
||||
"talk": "",
|
||||
"politics": ""
|
||||
},
|
||||
"misc": {
|
||||
"settings": "",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Speel af",
|
||||
"trailer": "Speel trailer af",
|
||||
"studio": "Studio",
|
||||
"studios": "Studio",
|
||||
"genre": "Genres",
|
||||
"genre-none": "Geen genres",
|
||||
"staff": "Personeel",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": "Wissel naar lijstweergave"
|
||||
},
|
||||
"genres": {
|
||||
"Action": "Actie",
|
||||
"Adventure": "Aventuur",
|
||||
"Animation": "Animatie",
|
||||
"Comedy": "Komedie",
|
||||
"Crime": "Crime",
|
||||
"Documentary": "Documantaire",
|
||||
"Drama": "Drama",
|
||||
"Family": "Famillie",
|
||||
"Fantasy": "Fantasie",
|
||||
"History": "Geschiedenis",
|
||||
"Horror": "Horror",
|
||||
"Music": "Muziek",
|
||||
"Mystery": "Mysterie",
|
||||
"Romance": "Romantiek",
|
||||
"ScienceFiction": "Science Fiction",
|
||||
"Thriller": "",
|
||||
"War": "Oorlog",
|
||||
"Western": "Western",
|
||||
"Kids": "Kinderen",
|
||||
"News": "Nieuws",
|
||||
"Reality": "",
|
||||
"Soap": "Soap",
|
||||
"Talk": "Talk",
|
||||
"Politics": "Politiek"
|
||||
"action": "Actie",
|
||||
"adventure": "Aventuur",
|
||||
"animation": "Animatie",
|
||||
"comedy": "Komedie",
|
||||
"crime": "Crime",
|
||||
"documentary": "Documantaire",
|
||||
"drama": "Drama",
|
||||
"family": "Famillie",
|
||||
"fantasy": "Fantasie",
|
||||
"history": "Geschiedenis",
|
||||
"horror": "Horror",
|
||||
"music": "Muziek",
|
||||
"mystery": "Mysterie",
|
||||
"romance": "Romantiek",
|
||||
"science-fiction": "Science Fiction",
|
||||
"thriller": "",
|
||||
"war": "Oorlog",
|
||||
"western": "Western",
|
||||
"kids": "Kinderen",
|
||||
"reality": "",
|
||||
"soap": "Soap",
|
||||
"talk": "Talk",
|
||||
"politics": "Politiek"
|
||||
},
|
||||
"misc": {
|
||||
"settings": "Instellingen",
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"null": "Oznacz jako nieobejrzane"
|
||||
},
|
||||
"season": "Sezon {{number}}",
|
||||
"studio": "Studio",
|
||||
"studios": "Studio",
|
||||
"genre": "Gatunek",
|
||||
"staff": "Obsada",
|
||||
"noOverview": "Brak dostępnego podsumowania",
|
||||
@ -210,30 +210,29 @@
|
||||
"empty": "Nie znaleziono wyniku. Spróbuj użyć innego zapytania."
|
||||
},
|
||||
"genres": {
|
||||
"Action": "Akcja",
|
||||
"Adventure": "Przygodowy",
|
||||
"Animation": "Animacja",
|
||||
"Comedy": "Komedia",
|
||||
"Crime": "Kryminał",
|
||||
"Documentary": "Dokument",
|
||||
"Drama": "Dramat",
|
||||
"Family": "Rodzinny",
|
||||
"Fantasy": "Fantastyka",
|
||||
"History": "Historyczny",
|
||||
"Horror": "Horror",
|
||||
"Music": "Muzyczny",
|
||||
"Mystery": "Tajemnica",
|
||||
"Romance": "Romans",
|
||||
"ScienceFiction": "Sci-Fi",
|
||||
"Thriller": "Dreszczowiec",
|
||||
"War": "Wojenny",
|
||||
"Western": "Dziki Zachód",
|
||||
"Kids": "Dziecięcy",
|
||||
"News": "Nowy",
|
||||
"Reality": "Realny",
|
||||
"Soap": "Opera mydlana",
|
||||
"Talk": "Dyskusja",
|
||||
"Politics": "Polityczny"
|
||||
"action": "Akcja",
|
||||
"adventure": "Przygodowy",
|
||||
"animation": "Animacja",
|
||||
"comedy": "Komedia",
|
||||
"crime": "Kryminał",
|
||||
"documentary": "Dokument",
|
||||
"drama": "Dramat",
|
||||
"family": "Rodzinny",
|
||||
"fantasy": "Fantastyka",
|
||||
"history": "Historyczny",
|
||||
"horror": "Horror",
|
||||
"music": "Muzyczny",
|
||||
"mystery": "Tajemnica",
|
||||
"romance": "Romans",
|
||||
"science-fiction": "Sci-Fi",
|
||||
"thriller": "Dreszczowiec",
|
||||
"war": "Wojenny",
|
||||
"western": "Dziki Zachód",
|
||||
"kids": "Dziecięcy",
|
||||
"reality": "Realny",
|
||||
"soap": "Opera mydlana",
|
||||
"talk": "Dyskusja",
|
||||
"politics": "Polityczny"
|
||||
},
|
||||
"navbar": {
|
||||
"home": "Ekran Główny",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Reproduzir",
|
||||
"trailer": "Reproduzir Trailer",
|
||||
"studio": "Estúdio",
|
||||
"studios": "Estúdio",
|
||||
"genre": "Gêneros",
|
||||
"genre-none": "Nenhum gênero",
|
||||
"staff": "Equipe",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": "Mudara para visualização de lista"
|
||||
},
|
||||
"genres": {
|
||||
"Action": "Ação",
|
||||
"Adventure": "Aventura",
|
||||
"Animation": "Animação",
|
||||
"Comedy": "Comédia",
|
||||
"Crime": "Crime",
|
||||
"Documentary": "Documentário",
|
||||
"Drama": "Drama",
|
||||
"Family": "Família",
|
||||
"Fantasy": "Fantasia",
|
||||
"History": "História",
|
||||
"Horror": "Terror",
|
||||
"Music": "Música",
|
||||
"Mystery": "Mistério",
|
||||
"Romance": "Romance",
|
||||
"ScienceFiction": "Ficção cientifica",
|
||||
"Thriller": "Suspense",
|
||||
"War": "Guerra",
|
||||
"Western": "Faroeste",
|
||||
"Kids": "Infantil",
|
||||
"News": "Notícias",
|
||||
"Reality": "Realidade",
|
||||
"Soap": "Novela",
|
||||
"Talk": "Entrevista",
|
||||
"Politics": "Política"
|
||||
"action": "Ação",
|
||||
"adventure": "Aventura",
|
||||
"animation": "Animação",
|
||||
"comedy": "Comédia",
|
||||
"crime": "Crime",
|
||||
"documentary": "Documentário",
|
||||
"drama": "Drama",
|
||||
"family": "Família",
|
||||
"fantasy": "Fantasia",
|
||||
"history": "História",
|
||||
"horror": "Terror",
|
||||
"music": "Música",
|
||||
"mystery": "Mistério",
|
||||
"romance": "Romance",
|
||||
"science-fiction": "Ficção cientifica",
|
||||
"thriller": "Suspense",
|
||||
"war": "Guerra",
|
||||
"western": "Faroeste",
|
||||
"kids": "Infantil",
|
||||
"reality": "Realidade",
|
||||
"soap": "Novela",
|
||||
"talk": "Entrevista",
|
||||
"politics": "Política"
|
||||
},
|
||||
"misc": {
|
||||
"settings": "Configurações",
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Reproduzir",
|
||||
"trailer": "Reproduzir trailer",
|
||||
"studio": "Estúdio",
|
||||
"studios": "Estúdio",
|
||||
"genre": "Géneros",
|
||||
"genre-none": "Nenhum género",
|
||||
"staff": "Equipa",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": "Mudara para visualização de lista"
|
||||
},
|
||||
"genres": {
|
||||
"Action": "Ação",
|
||||
"Adventure": "Aventura",
|
||||
"Animation": "Animação",
|
||||
"Comedy": "Comédia",
|
||||
"Crime": "Crime",
|
||||
"Documentary": "Documentário",
|
||||
"Drama": "Drama",
|
||||
"Family": "Família",
|
||||
"Fantasy": "Fantasia",
|
||||
"History": "História",
|
||||
"Horror": "Terror",
|
||||
"Music": "Música",
|
||||
"Mystery": "Mistério",
|
||||
"Romance": "Romance",
|
||||
"ScienceFiction": "Ficção cientifica",
|
||||
"Thriller": "Suspense",
|
||||
"War": "Guerra",
|
||||
"Western": "Faroeste",
|
||||
"Kids": "Infantil",
|
||||
"News": "Notícias",
|
||||
"Reality": "Realidade",
|
||||
"Soap": "Novela",
|
||||
"Talk": "Entrevista",
|
||||
"Politics": "Política"
|
||||
"action": "Ação",
|
||||
"adventure": "Aventura",
|
||||
"animation": "Animação",
|
||||
"comedy": "Comédia",
|
||||
"crime": "Crime",
|
||||
"documentary": "Documentário",
|
||||
"drama": "Drama",
|
||||
"family": "Família",
|
||||
"fantasy": "Fantasia",
|
||||
"history": "História",
|
||||
"horror": "Terror",
|
||||
"music": "Música",
|
||||
"mystery": "Mistério",
|
||||
"romance": "Romance",
|
||||
"science-fiction": "Ficção cientifica",
|
||||
"thriller": "Suspense",
|
||||
"war": "Guerra",
|
||||
"western": "Faroeste",
|
||||
"kids": "Infantil",
|
||||
"reality": "Realidade",
|
||||
"soap": "Novela",
|
||||
"talk": "Entrevista",
|
||||
"politics": "Política"
|
||||
},
|
||||
"misc": {
|
||||
"settings": "Configurações",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Redați",
|
||||
"trailer": "Redați trailerul",
|
||||
"studio": "Studio",
|
||||
"studios": "Studio",
|
||||
"genre": "Genuri",
|
||||
"genre-none": "Fără genuri",
|
||||
"staff": "Personalul",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": "Comutați la vizualizarea listă"
|
||||
},
|
||||
"genres": {
|
||||
"Action": "Acţiune",
|
||||
"Adventure": "Aventură",
|
||||
"Animation": "Animaţie",
|
||||
"Comedy": "Comedie",
|
||||
"Crime": "Crima",
|
||||
"Documentary": "Documentar",
|
||||
"Drama": "Dramă",
|
||||
"Family": "Familial",
|
||||
"Fantasy": "Fantezie",
|
||||
"History": "Istorie",
|
||||
"Horror": "Groază",
|
||||
"Music": "Muzică",
|
||||
"Mystery": "Mister",
|
||||
"Romance": "Romantism",
|
||||
"ScienceFiction": "Operă științifico-fantastică",
|
||||
"Thriller": "Thriller",
|
||||
"War": "Război",
|
||||
"Western": "de vest",
|
||||
"Kids": "Copii",
|
||||
"News": "Ştiri",
|
||||
"Reality": "Realitate",
|
||||
"Soap": "Novela",
|
||||
"Talk": "Vorbi",
|
||||
"Politics": "Politică"
|
||||
"action": "Acţiune",
|
||||
"adventure": "Aventură",
|
||||
"animation": "Animaţie",
|
||||
"comedy": "Comedie",
|
||||
"crime": "Crima",
|
||||
"documentary": "Documentar",
|
||||
"drama": "Dramă",
|
||||
"family": "Familial",
|
||||
"fantasy": "Fantezie",
|
||||
"history": "Istorie",
|
||||
"horror": "Groază",
|
||||
"music": "Muzică",
|
||||
"mystery": "Mister",
|
||||
"romance": "Romantism",
|
||||
"science-fiction": "Operă științifico-fantastică",
|
||||
"thriller": "Thriller",
|
||||
"war": "Război",
|
||||
"western": "de vest",
|
||||
"kids": "Copii",
|
||||
"reality": "Realitate",
|
||||
"soap": "Novela",
|
||||
"talk": "Vorbi",
|
||||
"politics": "Politică"
|
||||
},
|
||||
"misc": {
|
||||
"settings": "Setări",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Начать просмотр",
|
||||
"trailer": "Просмотр трейлера",
|
||||
"studio": "Студия",
|
||||
"studios": "Студия",
|
||||
"genre": "Жанры",
|
||||
"genre-none": "Жанры отсутствуют",
|
||||
"staff": "Команда",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": "Перейти в режим списка"
|
||||
},
|
||||
"genres": {
|
||||
"Action": "Экшн",
|
||||
"Adventure": "Приключение",
|
||||
"Animation": "Мультфильм",
|
||||
"Comedy": "Комедия",
|
||||
"Crime": "Криминал",
|
||||
"Documentary": "Документальный",
|
||||
"Drama": "Драма",
|
||||
"Family": "Семейный",
|
||||
"Fantasy": "Фэнтези",
|
||||
"History": "Исторический",
|
||||
"Horror": "Ужасы",
|
||||
"Music": "Музыкальный",
|
||||
"Mystery": "Мистический",
|
||||
"Romance": "Романтический",
|
||||
"ScienceFiction": "Научная фантастика",
|
||||
"Thriller": "Триллер",
|
||||
"War": "Военный",
|
||||
"Western": "Вестерн",
|
||||
"Kids": "Детский",
|
||||
"News": "Новости",
|
||||
"Reality": "Реалити-шоу",
|
||||
"Soap": "Мыльная опера",
|
||||
"Talk": "Ток-шоу",
|
||||
"Politics": "Политика"
|
||||
"action": "Экшн",
|
||||
"adventure": "Приключение",
|
||||
"animation": "Мультфильм",
|
||||
"comedy": "Комедия",
|
||||
"crime": "Криминал",
|
||||
"documentary": "Документальный",
|
||||
"drama": "Драма",
|
||||
"family": "Семейный",
|
||||
"fantasy": "Фэнтези",
|
||||
"history": "Исторический",
|
||||
"horror": "Ужасы",
|
||||
"music": "Музыкальный",
|
||||
"mystery": "Мистический",
|
||||
"romance": "Романтический",
|
||||
"science-fiction": "Научная фантастика",
|
||||
"thriller": "Триллер",
|
||||
"war": "Военный",
|
||||
"western": "Вестерн",
|
||||
"kids": "Детский",
|
||||
"reality": "Реалити-шоу",
|
||||
"soap": "Мыльная опера",
|
||||
"talk": "Ток-шоу",
|
||||
"politics": "Политика"
|
||||
},
|
||||
"misc": {
|
||||
"settings": "Настройки",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "விளையாடுங்கள்",
|
||||
"trailer": "டிரெய்லர் விளையாடுங்கள்",
|
||||
"studio": "ச்டுடியோ",
|
||||
"studios": "ச்டுடியோ",
|
||||
"genre": "வகைகள்",
|
||||
"genre-none": "வகைகள் இல்லை",
|
||||
"staff": "பணியாளர்",
|
||||
@ -70,30 +70,29 @@
|
||||
"switchToList": "பட்டியல் பார்வைக்கு மாறவும்"
|
||||
},
|
||||
"genres": {
|
||||
"Action": "செயல்",
|
||||
"Adventure": "துணிவு",
|
||||
"Animation": "அனிமேசன்",
|
||||
"Comedy": "நகைச்சுவை",
|
||||
"Crime": "குற்றம்",
|
||||
"Documentary": "ஆவணப்படம்",
|
||||
"Drama": "நாடகம்",
|
||||
"Family": "குடும்பம்",
|
||||
"Fantasy": "கற்பனை",
|
||||
"History": "வரலாறு",
|
||||
"Horror": "அதிர்ச்சி",
|
||||
"Music": "இசை",
|
||||
"Mystery": "மர்மம்",
|
||||
"Romance": "காதல்",
|
||||
"ScienceFiction": "அறிவியல் புனைகதை",
|
||||
"Thriller": "த்ரில்லர்",
|
||||
"War": "போர்",
|
||||
"Western": "மேற்கு",
|
||||
"Kids": "குழந்தைகள்",
|
||||
"News": "செய்தி",
|
||||
"Reality": "உண்மை",
|
||||
"Soap": "சோப்பு",
|
||||
"Talk": "பேச்சு",
|
||||
"Politics": "அரசியல்"
|
||||
"action": "செயல்",
|
||||
"adventure": "துணிவு",
|
||||
"animation": "அனிமேசன்",
|
||||
"comedy": "நகைச்சுவை",
|
||||
"crime": "குற்றம்",
|
||||
"documentary": "ஆவணப்படம்",
|
||||
"drama": "நாடகம்",
|
||||
"family": "குடும்பம்",
|
||||
"fantasy": "கற்பனை",
|
||||
"history": "வரலாறு",
|
||||
"horror": "அதிர்ச்சி",
|
||||
"music": "இசை",
|
||||
"mystery": "மர்மம்",
|
||||
"romance": "காதல்",
|
||||
"science-fiction": "அறிவியல் புனைகதை",
|
||||
"thriller": "த்ரில்லர்",
|
||||
"war": "போர்",
|
||||
"western": "மேற்கு",
|
||||
"kids": "குழந்தைகள்",
|
||||
"reality": "உண்மை",
|
||||
"soap": "சோப்பு",
|
||||
"talk": "பேச்சு",
|
||||
"politics": "அரசியல்"
|
||||
},
|
||||
"misc": {
|
||||
"settings": "அமைப்புகள்",
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "İzle",
|
||||
"trailer": "Fragmanı İzle",
|
||||
"studio": "Stüdyo",
|
||||
"studios": "Stüdyo",
|
||||
"genre": "Kategoriler",
|
||||
"genre-none": "Kategori bilgisi mevcut değil",
|
||||
"staff": "Kadro",
|
||||
@ -259,29 +259,28 @@
|
||||
}
|
||||
},
|
||||
"genres": {
|
||||
"Adventure": "Macera",
|
||||
"Comedy": "Komedi",
|
||||
"Crime": "Suç",
|
||||
"Documentary": "Belgesel",
|
||||
"Drama": "Dram",
|
||||
"Family": "Aile",
|
||||
"Fantasy": "Fantezi",
|
||||
"Horror": "Korku",
|
||||
"Music": "Müzikal",
|
||||
"Mystery": "Gizem",
|
||||
"Romance": "Romantik",
|
||||
"ScienceFiction": "Bilim Kurgu",
|
||||
"Thriller": "Gerilim",
|
||||
"War": "Savaş",
|
||||
"Western": "Kovboy",
|
||||
"Kids": "Çocuk",
|
||||
"News": "Haber",
|
||||
"Reality": "Reality",
|
||||
"Soap": "Melodrama",
|
||||
"Talk": "Söyleşi",
|
||||
"Action": "Aksiyon",
|
||||
"Animation": "Animasyon",
|
||||
"History": "Tarih",
|
||||
"Politics": "Siyaset"
|
||||
"adventure": "Macera",
|
||||
"comedy": "Komedi",
|
||||
"crime": "Suç",
|
||||
"documentary": "Belgesel",
|
||||
"drama": "Dram",
|
||||
"family": "Aile",
|
||||
"fantasy": "Fantezi",
|
||||
"horror": "Korku",
|
||||
"music": "Müzikal",
|
||||
"mystery": "Gizem",
|
||||
"romance": "Romantik",
|
||||
"science-fiction": "Bilim Kurgu",
|
||||
"thriller": "Gerilim",
|
||||
"war": "Savaş",
|
||||
"western": "Kovboy",
|
||||
"kids": "Çocuk",
|
||||
"reality": "Reality",
|
||||
"soap": "Melodrama",
|
||||
"talk": "Söyleşi",
|
||||
"action": "Aksiyon",
|
||||
"animation": "Animasyon",
|
||||
"history": "Tarih",
|
||||
"politics": "Siyaset"
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "Відтворити",
|
||||
"trailer": "Відтворити трейлер",
|
||||
"studio": "Студія",
|
||||
"studios": "Студія",
|
||||
"genre": "Жанри",
|
||||
"genre-none": "Жанри відсутні",
|
||||
"staff": "Команда",
|
||||
@ -261,29 +261,28 @@
|
||||
}
|
||||
},
|
||||
"genres": {
|
||||
"Horror": "Жахи",
|
||||
"War": "Війна",
|
||||
"Comedy": "Комедія",
|
||||
"Crime": "Кримінал",
|
||||
"Documentary": "Документальний",
|
||||
"Family": "Сімейний",
|
||||
"Fantasy": "Фентезі",
|
||||
"History": "Історія",
|
||||
"Music": "Музика",
|
||||
"Mystery": "Містика",
|
||||
"Romance": "Романтика",
|
||||
"ScienceFiction": "Наукова фантастика",
|
||||
"Thriller": "Трилер",
|
||||
"Western": "Вестерн",
|
||||
"Kids": "Діти",
|
||||
"News": "Новини",
|
||||
"Reality": "Реаліті",
|
||||
"Soap": "Мильна опера",
|
||||
"Talk": "Ток-шоу",
|
||||
"Politics": "Політика",
|
||||
"Action": "Екшн",
|
||||
"Adventure": "Пригода",
|
||||
"Animation": "Мультфільм",
|
||||
"Drama": "Драма"
|
||||
"horror": "Жахи",
|
||||
"war": "Війна",
|
||||
"comedy": "Комедія",
|
||||
"crime": "Кримінал",
|
||||
"documentary": "Документальний",
|
||||
"family": "Сімейний",
|
||||
"fantasy": "Фентезі",
|
||||
"history": "Історія",
|
||||
"music": "Музика",
|
||||
"mystery": "Містика",
|
||||
"romance": "Романтика",
|
||||
"science-fiction": "Наукова фантастика",
|
||||
"thriller": "Трилер",
|
||||
"western": "Вестерн",
|
||||
"kids": "Діти",
|
||||
"reality": "Реаліті",
|
||||
"soap": "Мильна опера",
|
||||
"talk": "Ток-шоу",
|
||||
"politics": "Політика",
|
||||
"action": "Екшн",
|
||||
"adventure": "Пригода",
|
||||
"animation": "Мультфільм",
|
||||
"drama": "Драма"
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"show": {
|
||||
"play": "播放",
|
||||
"trailer": "播放预告片",
|
||||
"studio": "制作公司",
|
||||
"studios": "制作公司",
|
||||
"genre": "类型",
|
||||
"genre-none": "无类型",
|
||||
"staff": "制作人员",
|
||||
@ -257,29 +257,28 @@
|
||||
}
|
||||
},
|
||||
"genres": {
|
||||
"Action": "动作片",
|
||||
"Adventure": "冒险",
|
||||
"Animation": "动漫",
|
||||
"Comedy": "喜剧片",
|
||||
"Crime": "犯罪片",
|
||||
"Documentary": "纪录片",
|
||||
"Drama": "戏剧",
|
||||
"Family": "家庭",
|
||||
"Fantasy": "奇幻",
|
||||
"History": "历史",
|
||||
"Horror": "恐怖",
|
||||
"Music": "音乐",
|
||||
"Mystery": "悬疑",
|
||||
"Romance": "浪漫",
|
||||
"ScienceFiction": "科幻",
|
||||
"Thriller": "惊悚",
|
||||
"War": "战争",
|
||||
"Western": "西部",
|
||||
"Kids": "儿童",
|
||||
"News": "新闻",
|
||||
"Reality": "现实",
|
||||
"Soap": "肥皂剧",
|
||||
"Talk": "访谈",
|
||||
"Politics": "政治"
|
||||
"action": "动作片",
|
||||
"adventure": "冒险",
|
||||
"animation": "动漫",
|
||||
"comedy": "喜剧片",
|
||||
"crime": "犯罪片",
|
||||
"documentary": "纪录片",
|
||||
"drama": "戏剧",
|
||||
"family": "家庭",
|
||||
"fantasy": "奇幻",
|
||||
"history": "历史",
|
||||
"horror": "恐怖",
|
||||
"music": "音乐",
|
||||
"mystery": "悬疑",
|
||||
"romance": "浪漫",
|
||||
"science-fiction": "科幻",
|
||||
"thriller": "惊悚",
|
||||
"war": "战争",
|
||||
"western": "西部",
|
||||
"kids": "儿童",
|
||||
"reality": "现实",
|
||||
"soap": "肥皂剧",
|
||||
"talk": "访谈",
|
||||
"politics": "政治"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { readdir , mkdir } from 'node:fs/promises';
|
||||
import { mkdir, readdir } from "node:fs/promises";
|
||||
|
||||
async function jassub() {
|
||||
const srcDir = new URL("../node_modules/jassub/dist/", import.meta.url);
|
||||
const destDir = new URL("../public/jassub/", import.meta.url);
|
||||
|
||||
@ -10,3 +11,46 @@ for (const file of files) {
|
||||
const src = await Bun.file(new URL(file, srcDir)).arrayBuffer();
|
||||
await Bun.write(new URL(file, destDir), src);
|
||||
}
|
||||
}
|
||||
|
||||
async function translations() {
|
||||
const srcDir = new URL("../public/translations/", import.meta.url);
|
||||
const dest = new URL(
|
||||
"../src/providers/translations.compile.ts",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
const translations = (await readdir(srcDir))
|
||||
.map((x) => ({
|
||||
file: x,
|
||||
lang: x.replace(".json", ""),
|
||||
var: x.replace(".json", "").replace("-", "_"),
|
||||
}))
|
||||
.map((x) => ({
|
||||
...x,
|
||||
quotedLang: x.lang.includes("-") ? `"${x.lang}"` : x.lang,
|
||||
}))
|
||||
.sort((a, b) => a.lang.localeCompare(b.lang));
|
||||
await Bun.write(
|
||||
dest,
|
||||
`// this file is auto-generated via a postinstall script.
|
||||
|
||||
${translations
|
||||
.map((x) => `import ${x.var} from "../../public/translations/${x.file}";`)
|
||||
.join("\n")}
|
||||
|
||||
export const resources = {
|
||||
${translations
|
||||
.map((x) => `${x.quotedLang}: { translation: ${x.var} },`)
|
||||
.join("\n\t")}
|
||||
};
|
||||
|
||||
export const supportedLanguages = [
|
||||
${translations.map((x) => `"${x.lang}",`).join("\n\t")}
|
||||
];
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
await jassub();
|
||||
await translations();
|
||||
|
||||
3
front/src/app/(app)/settings.tsx
Normal file
3
front/src/app/(app)/settings.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import { SettingsPage } from "~/ui/settings";
|
||||
|
||||
export default SettingsPage;
|
||||
@ -2,7 +2,7 @@ import { Stack } from "expo-router";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useTheme } from "yoshiki/native";
|
||||
import { ErrorConsumer } from "~/providers/error-consumer";
|
||||
import { NavbarTitle } from "~/ui/navbar";
|
||||
import { NavbarProfile, NavbarTitle } from "~/ui/navbar";
|
||||
|
||||
export default function Layout() {
|
||||
const insets = useSafeAreaInsets();
|
||||
@ -13,6 +13,7 @@ export default function Layout() {
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerTitle: () => <NavbarTitle />,
|
||||
headerRight: () => <NavbarProfile />,
|
||||
contentStyle: {
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
|
||||
3
front/src/app/(public)/settings.tsx
Normal file
3
front/src/app/(public)/settings.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import { SettingsPage } from "~/ui/settings";
|
||||
|
||||
export default SettingsPage;
|
||||
@ -2,32 +2,37 @@ import { z } from "zod/v4";
|
||||
|
||||
export const User = z
|
||||
.object({
|
||||
// // keep a default for older versions of the api
|
||||
// .default({}),
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
email: z.string(),
|
||||
// permissions: z.array(z.string()),
|
||||
claims: z.object({
|
||||
permissions: z.array(z.string()),
|
||||
// hasPassword: z.boolean().default(true),
|
||||
// settings: z
|
||||
// .object({
|
||||
// downloadQuality: z
|
||||
// .union([
|
||||
// z.literal("original"),
|
||||
// z.literal("8k"),
|
||||
// z.literal("4k"),
|
||||
// z.literal("1440p"),
|
||||
// z.literal("1080p"),
|
||||
// z.literal("720p"),
|
||||
// z.literal("480p"),
|
||||
// z.literal("360p"),
|
||||
// z.literal("240p"),
|
||||
// ])
|
||||
// .default("original")
|
||||
// .catch("original"),
|
||||
// audioLanguage: z.string().default("default").catch("default"),
|
||||
// subtitleLanguage: z.string().nullable().default(null).catch(null),
|
||||
// })
|
||||
// // keep a default for older versions of the api
|
||||
// .default({}),
|
||||
settings: z
|
||||
.object({
|
||||
downloadQuality: z
|
||||
.union([
|
||||
z.literal("original"),
|
||||
z.literal("8k"),
|
||||
z.literal("4k"),
|
||||
z.literal("1440p"),
|
||||
z.literal("1080p"),
|
||||
z.literal("720p"),
|
||||
z.literal("480p"),
|
||||
z.literal("360p"),
|
||||
z.literal("240p"),
|
||||
])
|
||||
.catch("original"),
|
||||
audioLanguage: z.string().catch("default"),
|
||||
subtitleLanguage: z.string().nullable().catch(null),
|
||||
})
|
||||
.default({
|
||||
downloadQuality: "original",
|
||||
audioLanguage: "default",
|
||||
subtitleLanguage: null,
|
||||
}),
|
||||
// externalId: z
|
||||
// .record(
|
||||
// z.string(),
|
||||
@ -38,6 +43,7 @@ export const User = z
|
||||
// }),
|
||||
// )
|
||||
// .default({}),
|
||||
}),
|
||||
})
|
||||
.transform((x) => ({
|
||||
...x,
|
||||
|
||||
@ -1,23 +1,3 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681
|
||||
|
||||
import type { AlertButton, AlertOptions } from "react-native";
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
export { Footer, Header, Main, Nav, UL } from "@expo/html-elements";
|
||||
// export * from "./snackbar";
|
||||
export * from "./alert";
|
||||
export * from "./avatar";
|
||||
export * from "./button";
|
||||
export * from "./chip";
|
||||
@ -9,11 +11,9 @@ export * from "./image";
|
||||
export * from "./image-background";
|
||||
export * from "./input";
|
||||
export * from "./links";
|
||||
// export * from "./snackbar";
|
||||
// export * from "./alert";
|
||||
export * from "./menu";
|
||||
export * from "./popup";
|
||||
export * from "./progress";
|
||||
// export * from "./popup";
|
||||
export * from "./select";
|
||||
export * from "./skeleton";
|
||||
export * from "./slider";
|
||||
|
||||
@ -1,30 +1,9 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { usePortal } from "@gorhom/portal";
|
||||
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { px, vh } from "yoshiki/native";
|
||||
import { imageBorderRadius } from "./constants";
|
||||
import { Container } from "./container";
|
||||
import { ContrastArea, SwitchVariant, type YoshikiFunc } from "./themes";
|
||||
import { ContrastArea, SwitchVariant, type YoshikiFunc } from "./theme";
|
||||
import { ts } from "./utils";
|
||||
|
||||
export const Popup = ({
|
||||
@ -52,7 +31,7 @@ export const Popup = ({
|
||||
<Container
|
||||
{...css(
|
||||
{
|
||||
borderRadius: px(imageBorderRadius),
|
||||
borderRadius: px(6),
|
||||
paddingHorizontal: 0,
|
||||
bg: (theme) => theme.background,
|
||||
overflow: "hidden",
|
||||
|
||||
@ -13,8 +13,12 @@ const writeAccounts = (accounts: Account[]) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const readAccounts = () => {
|
||||
return readValue("accounts", z.array(Account)) ?? [];
|
||||
};
|
||||
|
||||
export const addAccount = (account: Account) => {
|
||||
const accounts = readValue("accounts", z.array(Account)) ?? [];
|
||||
const accounts = readAccounts();
|
||||
|
||||
// Prevent the user from adding the same account twice.
|
||||
if (accounts.find((x) => x.id === account.id)) {
|
||||
@ -29,7 +33,7 @@ export const addAccount = (account: Account) => {
|
||||
};
|
||||
|
||||
export const removeAccounts = (filter: (acc: Account) => boolean) => {
|
||||
let accounts = readValue("accounts", z.array(Account)) ?? [];
|
||||
let accounts = readAccounts();
|
||||
accounts = accounts.filter((x) => !filter(x));
|
||||
if (!accounts.find((x) => x.selected) && accounts.length > 0) {
|
||||
accounts[0].selected = true;
|
||||
@ -38,7 +42,7 @@ export const removeAccounts = (filter: (acc: Account) => boolean) => {
|
||||
};
|
||||
|
||||
export const updateAccount = (id: string, account: Account) => {
|
||||
const accounts = readValue("accounts", z.array(Account)) ?? [];
|
||||
const accounts = readAccounts();
|
||||
const idx = accounts.findIndex((x) => x.id === id);
|
||||
if (idx === -1) return;
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { PortalProvider } from "@gorhom/portal";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export const NativeProviders = ({ children }: { children: ReactNode }) => {
|
||||
return children;
|
||||
return <PortalProvider>{children}</PortalProvider>;
|
||||
};
|
||||
|
||||
3
front/src/providers/translations-detector.tsx
Normal file
3
front/src/providers/translations-detector.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import { createLanguageDetector } from "react-native-localization-settings";
|
||||
|
||||
export const languageDetector = createLanguageDetector({});
|
||||
7
front/src/providers/translations-detector.web.tsx
Normal file
7
front/src/providers/translations-detector.web.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
export const languageDetector = new LanguageDetector(null, {
|
||||
order: ["querystring", "cookie", "navigator", "path", "subdomain"],
|
||||
caches: ["cookie"],
|
||||
cookieMinutes: 525600, // 1 years
|
||||
});
|
||||
@ -1,22 +1,71 @@
|
||||
// this file is run at compile time thanks to a vite plugin
|
||||
|
||||
// import { readFile, readdir } from "node:fs/promises";
|
||||
// import type { Resource } from "i18next";
|
||||
|
||||
// const translationDir = new URL("../../public/translations/", import.meta.url);
|
||||
// const langs = await readdir(translationDir);
|
||||
|
||||
// export const resources: Resource = Object.fromEntries(
|
||||
// await Promise.all(
|
||||
// langs.map(async (x) => [
|
||||
// x.replace(".json", ""),
|
||||
// { translation: JSON.parse(await readFile(new URL(x, translationDir), "utf8")) },
|
||||
// ]),
|
||||
// ),
|
||||
// );
|
||||
// this file is auto-generated via a postinstall script.
|
||||
|
||||
import am from "../../public/translations/am.json";
|
||||
import ar from "../../public/translations/ar.json";
|
||||
import de from "../../public/translations/de.json";
|
||||
import en from "../../public/translations/en.json";
|
||||
import es from "../../public/translations/es.json";
|
||||
import fr from "../../public/translations/fr.json";
|
||||
import gl from "../../public/translations/gl.json";
|
||||
import is from "../../public/translations/is.json";
|
||||
import it from "../../public/translations/it.json";
|
||||
import ko from "../../public/translations/ko.json";
|
||||
import ml from "../../public/translations/ml.json";
|
||||
import nl from "../../public/translations/nl.json";
|
||||
import pl from "../../public/translations/pl.json";
|
||||
import pt from "../../public/translations/pt.json";
|
||||
import pt_BR from "../../public/translations/pt-BR.json";
|
||||
import ro from "../../public/translations/ro.json";
|
||||
import ru from "../../public/translations/ru.json";
|
||||
import ta from "../../public/translations/ta.json";
|
||||
import tr from "../../public/translations/tr.json";
|
||||
import uk from "../../public/translations/uk.json";
|
||||
import zh from "../../public/translations/zh.json";
|
||||
|
||||
export const resources = { en };
|
||||
export const resources = {
|
||||
am: { translation: am },
|
||||
ar: { translation: ar },
|
||||
de: { translation: de },
|
||||
en: { translation: en },
|
||||
es: { translation: es },
|
||||
fr: { translation: fr },
|
||||
gl: { translation: gl },
|
||||
is: { translation: is },
|
||||
it: { translation: it },
|
||||
ko: { translation: ko },
|
||||
ml: { translation: ml },
|
||||
nl: { translation: nl },
|
||||
pl: { translation: pl },
|
||||
pt: { translation: pt },
|
||||
"pt-BR": { translation: pt_BR },
|
||||
ro: { translation: ro },
|
||||
ru: { translation: ru },
|
||||
ta: { translation: ta },
|
||||
tr: { translation: tr },
|
||||
uk: { translation: uk },
|
||||
zh: { translation: zh },
|
||||
};
|
||||
|
||||
export const supportedLanguages = Object.keys(resources);
|
||||
export const supportedLanguages = [
|
||||
"am",
|
||||
"ar",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"gl",
|
||||
"is",
|
||||
"it",
|
||||
"ko",
|
||||
"ml",
|
||||
"nl",
|
||||
"pl",
|
||||
"pt",
|
||||
"pt-BR",
|
||||
"ro",
|
||||
"ru",
|
||||
"ta",
|
||||
"tr",
|
||||
"uk",
|
||||
"zh",
|
||||
];
|
||||
|
||||
@ -3,11 +3,12 @@ import { type ReactNode, useMemo } from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { setServerData } from "~/utils";
|
||||
import { resources, supportedLanguages } from "./translations.compile";
|
||||
import { languageDetector } from "./translations-detector";
|
||||
|
||||
export const TranslationsProvider = ({ children }: { children: ReactNode }) => {
|
||||
const val = useMemo(() => {
|
||||
const i18n = i18next.createInstance();
|
||||
i18n.init({
|
||||
i18n.use(languageDetector).init({
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
||||
@ -4,12 +4,15 @@ import { type ReactNode, useMemo } from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { getServerData } from "~/utils";
|
||||
import { supportedLanguages } from "./translations.compile";
|
||||
import { languageDetector } from "./translations-detector";
|
||||
|
||||
export const TranslationsProvider = ({ children }: { children: ReactNode }) => {
|
||||
const val = useMemo(() => {
|
||||
const i18n = i18next.createInstance();
|
||||
// TODO: use https://github.com/i18next/i18next-browser-languageDetector
|
||||
i18n.use(HttpApi).init<HttpBackendOptions>({
|
||||
i18n
|
||||
.use(HttpApi)
|
||||
.use(languageDetector)
|
||||
.init<HttpBackendOptions>({
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
||||
@ -260,7 +260,7 @@ export const prefetch = async (...queries: QueryIdentifier[]) => {
|
||||
};
|
||||
|
||||
type MutationParams = {
|
||||
method?: "POST" | "PUT" | "DELETE";
|
||||
method?: "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
path?: string[];
|
||||
params?: {
|
||||
[query: string]: boolean | number | string | string[] | undefined;
|
||||
@ -268,12 +268,14 @@ type MutationParams = {
|
||||
body?: object;
|
||||
};
|
||||
|
||||
export const useMutation = <T = void>({
|
||||
export const useMutation = <T = void, QueryRet = void>({
|
||||
compute,
|
||||
invalidate,
|
||||
optimistic,
|
||||
...queryParams
|
||||
}: MutationParams & {
|
||||
compute?: (param: T) => MutationParams;
|
||||
optimistic?: (param: T) => QueryRet;
|
||||
invalidate: string[] | null;
|
||||
}) => {
|
||||
const { apiUrl, authToken } = useContext(AccountContext);
|
||||
@ -293,14 +295,35 @@ export const useMutation = <T = void>({
|
||||
parser: null,
|
||||
});
|
||||
},
|
||||
onSuccess: invalidate
|
||||
? async () =>
|
||||
...(invalidate && optimistic
|
||||
? {
|
||||
onMutate: async (params) => {
|
||||
const next = optimistic(params);
|
||||
const queryKey = toQueryKey({ apiUrl, path: invalidate });
|
||||
await queryClient.cancelQueries({
|
||||
queryKey,
|
||||
});
|
||||
const previous = queryClient.getQueryData(queryKey);
|
||||
queryClient.setQueryData(queryKey, next);
|
||||
return { previous, next };
|
||||
},
|
||||
onError: (_, __, context) => {
|
||||
queryClient.setQueryData(
|
||||
toQueryKey({ apiUrl, path: invalidate }),
|
||||
context!.previous,
|
||||
);
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(invalidate
|
||||
? {
|
||||
onSettled: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: toQueryKey({ apiUrl, path: invalidate }),
|
||||
})
|
||||
: undefined,
|
||||
// TODO: Do something
|
||||
// onError: () => {}
|
||||
});
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
return mutation;
|
||||
};
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { z } from "zod/v4";
|
||||
import { type Account, type KyooError, User } from "~/models";
|
||||
import { addAccount, removeAccounts } from "~/providers/account-store";
|
||||
import {
|
||||
addAccount,
|
||||
readAccounts,
|
||||
removeAccounts,
|
||||
} from "~/providers/account-store";
|
||||
import { queryFn } from "~/query";
|
||||
|
||||
type Result<A, B> =
|
||||
@ -77,15 +81,30 @@ export const login = async (
|
||||
// }
|
||||
// };
|
||||
|
||||
export const logout = () => {
|
||||
export const logout = async () => {
|
||||
const accounts = readAccounts();
|
||||
const account = accounts.find((x) => x.selected);
|
||||
if (account) {
|
||||
await queryFn({
|
||||
method: "DELETE",
|
||||
url: "auth/sessions/current",
|
||||
authToken: account.token,
|
||||
parser: null,
|
||||
});
|
||||
}
|
||||
removeAccounts((x) => x.selected);
|
||||
};
|
||||
|
||||
export const deleteAccount = async () => {
|
||||
// await queryFn({
|
||||
// method: "DELETE",
|
||||
// url: "auth/users/me",
|
||||
// parser: null,
|
||||
// });
|
||||
const accounts = readAccounts();
|
||||
const account = accounts.find((x) => x.selected);
|
||||
if (account) {
|
||||
await queryFn({
|
||||
method: "DELETE",
|
||||
url: "auth/users/me",
|
||||
authToken: account.token,
|
||||
parser: null,
|
||||
});
|
||||
}
|
||||
logout();
|
||||
};
|
||||
|
||||
@ -66,7 +66,7 @@ export const LoginPage = () => {
|
||||
{/* )} */}
|
||||
<P>
|
||||
<Trans i18nKey="login.or-register">
|
||||
Don’t have an account?{" "}
|
||||
Don’t have an account?
|
||||
<A href={`/register?apiUrl=${apiUrl}`}>Register</A>.
|
||||
</Trans>
|
||||
</P>
|
||||
|
||||
@ -46,14 +46,14 @@ export const RegisterPage = () => {
|
||||
|
||||
<P {...(css({ paddingLeft: ts(1) }) as any)}>{t("login.password")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="password-new"
|
||||
autoComplete="new-password"
|
||||
variant="big"
|
||||
onChangeText={(value) => setPassword(value)}
|
||||
/>
|
||||
|
||||
<P {...(css({ paddingLeft: ts(1) }) as any)}>{t("login.confirm")}</P>
|
||||
<PasswordInput
|
||||
autoComplete="password-new"
|
||||
autoComplete="new-password"
|
||||
variant="big"
|
||||
onChangeText={(value) => setConfirm(value)}
|
||||
/>
|
||||
@ -90,7 +90,7 @@ export const RegisterPage = () => {
|
||||
{/* )} */}
|
||||
<P>
|
||||
<Trans i18nKey="login.or-login">
|
||||
Have an account already?{" "}
|
||||
Have an account already?
|
||||
<A href={`/login?apiUrl=${apiUrl}`}>Log in</A>.
|
||||
</Trans>
|
||||
</P>
|
||||
|
||||
@ -1,81 +1,67 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
type Account,
|
||||
type KyooErrors,
|
||||
deleteAccount,
|
||||
logout,
|
||||
queryFn,
|
||||
useAccount,
|
||||
} from "@kyoo/models";
|
||||
import { Alert, Avatar, Button, H1, Icon, Input, P, Popup, ts, usePopup } from "@kyoo/primitives";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import Username from "@material-symbols/svg-400/outlined/badge.svg";
|
||||
import Mail from "@material-symbols/svg-400/outlined/mail.svg";
|
||||
import Password from "@material-symbols/svg-400/outlined/password.svg";
|
||||
// import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg";
|
||||
import Delete from "@material-symbols/svg-400/rounded/delete.svg";
|
||||
import Logout from "@material-symbols/svg-400/rounded/logout.svg";
|
||||
// import * as ImagePicker from "expo-image-picker";
|
||||
import { type ComponentProps, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { rem, useYoshiki } from "yoshiki/native";
|
||||
import { PasswordInput } from "../../../../src/ui/login/password-input";
|
||||
import type { KyooError, User } from "~/models";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
H1,
|
||||
Icon,
|
||||
Input,
|
||||
P,
|
||||
Popup,
|
||||
ts,
|
||||
usePopup,
|
||||
} from "~/primitives";
|
||||
import { useAccount } from "~/providers/account-context";
|
||||
import { useMutation } from "~/query";
|
||||
import { deleteAccount, logout } from "../login/logic";
|
||||
import { PasswordInput } from "../login/password-input";
|
||||
import { Preference, SettingsContainer } from "./base";
|
||||
|
||||
import Username from "@material-symbols/svg-400/outlined/badge.svg";
|
||||
import Mail from "@material-symbols/svg-400/outlined/mail.svg";
|
||||
import Password from "@material-symbols/svg-400/outlined/password.svg";
|
||||
import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg";
|
||||
import Delete from "@material-symbols/svg-400/rounded/delete.svg";
|
||||
import Logout from "@material-symbols/svg-400/rounded/logout.svg";
|
||||
|
||||
function dataURItoBlob(dataURI: string) {
|
||||
const byteString = atob(dataURI.split(",")[1]);
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
return new Blob([ab], { type: "image/jpeg" });
|
||||
}
|
||||
// function dataURItoBlob(dataURI: string) {
|
||||
// const byteString = atob(dataURI.split(",")[1]);
|
||||
// const ab = new ArrayBuffer(byteString.length);
|
||||
// const ia = new Uint8Array(ab);
|
||||
// for (let i = 0; i < byteString.length; i++) {
|
||||
// ia[i] = byteString.charCodeAt(i);
|
||||
// }
|
||||
// return new Blob([ab], { type: "image/jpeg" });
|
||||
// }
|
||||
|
||||
export const AccountSettings = () => {
|
||||
const account = useAccount()!;
|
||||
const { css, theme } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const [setPopup, close] = usePopup();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { mutateAsync } = useMutation({
|
||||
mutationFn: async (update: Partial<Account>) =>
|
||||
await queryFn({
|
||||
path: ["auth", "me"],
|
||||
method: "PATCH",
|
||||
body: update,
|
||||
path: ["auth", "users", "me"],
|
||||
compute: (update: Partial<User>) => ({ body: update }),
|
||||
optimistic: (update) => ({
|
||||
...account,
|
||||
...update,
|
||||
claims: { ...account.claims, ...update.claims },
|
||||
}),
|
||||
onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["auth", "me"] }),
|
||||
invalidate: ["auth", "users", "me"],
|
||||
});
|
||||
|
||||
const { mutateAsync: editPassword } = useMutation({
|
||||
mutationFn: async (request: { newPassword: string; oldPassword: string }) =>
|
||||
await queryFn({
|
||||
path: ["auth", "password-reset"],
|
||||
method: "POST",
|
||||
body: request,
|
||||
method: "PATCH",
|
||||
path: ["auth", "users", "me", "password"],
|
||||
compute: (body: { oldPassword: string; newPassword: string }) => ({
|
||||
body,
|
||||
}),
|
||||
invalidate: null,
|
||||
});
|
||||
|
||||
return (
|
||||
@ -106,7 +92,8 @@ export const AccountSettings = () => {
|
||||
],
|
||||
{
|
||||
cancelable: true,
|
||||
userInterfaceStyle: theme.mode === "auto" ? "light" : theme.mode,
|
||||
userInterfaceStyle:
|
||||
theme.mode === "auto" ? "light" : theme.mode,
|
||||
icon: "warning",
|
||||
},
|
||||
);
|
||||
@ -137,43 +124,47 @@ export const AccountSettings = () => {
|
||||
}
|
||||
/>
|
||||
</Preference>
|
||||
{/* <Preference */}
|
||||
{/* icon={AccountCircle} */}
|
||||
{/* customIcon={<Avatar src={account.logo} />} */}
|
||||
{/* label={t("settings.account.avatar.label")} */}
|
||||
{/* description={t("settings.account.avatar.description")} */}
|
||||
{/* > */}
|
||||
{/* <Button */}
|
||||
{/* text={t("misc.edit")} */}
|
||||
{/* onPress={async () => { */}
|
||||
{/* const img = await ImagePicker.launchImageLibraryAsync({ */}
|
||||
{/* mediaTypes: ImagePicker.MediaTypeOptions.Images, */}
|
||||
{/* aspect: [1, 1], */}
|
||||
{/* quality: 1, */}
|
||||
{/* base64: true, */}
|
||||
{/* }); */}
|
||||
{/* if (img.canceled || img.assets.length !== 1) return; */}
|
||||
{/* const data = dataURItoBlob(img.assets[0].uri); */}
|
||||
{/* const formData = new FormData(); */}
|
||||
{/* formData.append("picture", data); */}
|
||||
{/* await queryFn({ */}
|
||||
{/* method: "POST", */}
|
||||
{/* path: ["auth", "me", "logo"], */}
|
||||
{/* formData, */}
|
||||
{/* }); */}
|
||||
{/* }} */}
|
||||
{/* /> */}
|
||||
{/* <Button */}
|
||||
{/* text={t("misc.delete")} */}
|
||||
{/* onPress={async () => { */}
|
||||
{/* await queryFn({ */}
|
||||
{/* method: "DELETE", */}
|
||||
{/* path: ["auth", "me", "logo"], */}
|
||||
{/* }); */}
|
||||
{/* }} */}
|
||||
{/* /> */}
|
||||
{/* </Preference> */}
|
||||
<Preference
|
||||
icon={AccountCircle}
|
||||
customIcon={<Avatar src={account.logo} />}
|
||||
label={t("settings.account.avatar.label")}
|
||||
description={t("settings.account.avatar.description")}
|
||||
icon={Mail}
|
||||
label={t("settings.account.email.label")}
|
||||
description={account.email}
|
||||
>
|
||||
<Button
|
||||
text={t("misc.edit")}
|
||||
onPress={async () => {
|
||||
const img = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
aspect: [1, 1],
|
||||
quality: 1,
|
||||
base64: true,
|
||||
});
|
||||
if (img.canceled || img.assets.length !== 1) return;
|
||||
const data = dataURItoBlob(img.assets[0].uri);
|
||||
const formData = new FormData();
|
||||
formData.append("picture", data);
|
||||
await queryFn({
|
||||
method: "POST",
|
||||
path: ["auth", "me", "logo"],
|
||||
formData,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
text={t("misc.delete")}
|
||||
onPress={async () => {
|
||||
await queryFn({
|
||||
method: "DELETE",
|
||||
path: ["auth", "me", "logo"],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Preference>
|
||||
<Preference icon={Mail} label={t("settings.account.email.label")} description={account.email}>
|
||||
<Button
|
||||
text={t("misc.edit")}
|
||||
onPress={() =>
|
||||
@ -202,8 +193,10 @@ export const AccountSettings = () => {
|
||||
<ChangePasswordPopup
|
||||
icon={Password}
|
||||
label={t("settings.account.password.label")}
|
||||
hasPassword={account.hasPassword}
|
||||
apply={async (op, np) => await editPassword({ oldPassword: op, newPassword: np })}
|
||||
hasPassword={true}
|
||||
apply={async (op, np) =>
|
||||
await editPassword({ oldPassword: op, newPassword: np })
|
||||
}
|
||||
close={close}
|
||||
/>,
|
||||
)
|
||||
@ -236,7 +229,9 @@ const ChangePopup = ({
|
||||
<Popup>
|
||||
{({ css }) => (
|
||||
<>
|
||||
<View {...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}>
|
||||
<View
|
||||
{...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}
|
||||
>
|
||||
<Icon icon={icon} />
|
||||
<H1 {...css({ fontSize: rem(2) })}>{label}</H1>
|
||||
</View>
|
||||
@ -246,7 +241,13 @@ const ChangePopup = ({
|
||||
value={value}
|
||||
onChangeText={(v) => setValue(v)}
|
||||
/>
|
||||
<View {...css({ flexDirection: "row", alignSelf: "flex-end", gap: ts(1) })}>
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
alignSelf: "flex-end",
|
||||
gap: ts(1),
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
text={t("misc.cancel")}
|
||||
onPress={() => close()}
|
||||
@ -289,7 +290,9 @@ const ChangePasswordPopup = ({
|
||||
<Popup>
|
||||
{({ css }) => (
|
||||
<>
|
||||
<View {...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}>
|
||||
<View
|
||||
{...css({ flexDirection: "row", alignItems: "center", gap: ts(2) })}
|
||||
>
|
||||
<Icon icon={icon} />
|
||||
<H1 {...css({ fontSize: rem(2) })}>{label}</H1>
|
||||
</View>
|
||||
@ -303,14 +306,22 @@ const ChangePasswordPopup = ({
|
||||
/>
|
||||
)}
|
||||
<PasswordInput
|
||||
autoComplete="password-new"
|
||||
autoComplete="new-password"
|
||||
variant="big"
|
||||
value={newValue}
|
||||
onChangeText={(v) => setNewValue(v)}
|
||||
placeholder={t("settings.account.password.newPassword")}
|
||||
/>
|
||||
{error && <P {...css({ color: (theme) => theme.colors.red })}>{error}</P>}
|
||||
<View {...css({ flexDirection: "row", alignSelf: "flex-end", gap: ts(1) })}>
|
||||
{error && (
|
||||
<P {...css({ color: (theme) => theme.colors.red })}>{error}</P>
|
||||
)}
|
||||
<View
|
||||
{...css({
|
||||
flexDirection: "row",
|
||||
alignSelf: "flex-end",
|
||||
gap: ts(1),
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
text={t("misc.cancel")}
|
||||
onPress={() => close()}
|
||||
@ -323,7 +334,7 @@ const ChangePasswordPopup = ({
|
||||
await apply(oldValue, newValue);
|
||||
close();
|
||||
} catch (e) {
|
||||
setError((e as KyooErrors).errors[0]);
|
||||
setError((e as KyooError).message);
|
||||
}
|
||||
}}
|
||||
{...css({ minWidth: rem(6) })}
|
||||
@ -1,24 +1,7 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { type User, queryFn, useAccount } from "@kyoo/models";
|
||||
import { Children, type ReactElement, type ReactNode } from "react";
|
||||
import { type Falsy, View } from "react-native";
|
||||
import { percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||
import type { User } from "~/models";
|
||||
import {
|
||||
Container,
|
||||
H1,
|
||||
@ -27,13 +10,10 @@ import {
|
||||
P,
|
||||
SubP,
|
||||
SwitchVariant,
|
||||
imageBorderRadius,
|
||||
ts,
|
||||
} from "@kyoo/primitives";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Children, type ReactElement, type ReactNode } from "react";
|
||||
import { type Falsy, View } from "react-native";
|
||||
import { percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||
} from "~/primitives";
|
||||
import { useAccount } from "~/providers/account-context";
|
||||
import { useMutation } from "~/query";
|
||||
|
||||
export const Preference = ({
|
||||
customIcon,
|
||||
@ -75,7 +55,7 @@ export const Preference = ({
|
||||
>
|
||||
{customIcon ?? <Icon icon={icon} />}
|
||||
<View {...css({ flexShrink: 1 })}>
|
||||
<P {...css({ marginBottom: 0 })}>{label}</P>
|
||||
<P {...(css({ marginBottom: 0 }) as any)}>{label}</P>
|
||||
<SubP>{description}</SubP>
|
||||
</View>
|
||||
</View>
|
||||
@ -118,7 +98,7 @@ export const SettingsContainer = ({
|
||||
<View
|
||||
{...css({
|
||||
bg: (theme) => theme.background,
|
||||
borderRadius: px(imageBorderRadius),
|
||||
borderRadius: px(6),
|
||||
})}
|
||||
>
|
||||
{Children.map(children, (x, i) => (
|
||||
@ -135,34 +115,34 @@ export const SettingsContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const useSetting = <Setting extends keyof User["settings"]>(setting: Setting) => {
|
||||
export const useSetting = <Setting extends keyof User["claims"]["settings"]>(
|
||||
setting: Setting,
|
||||
) => {
|
||||
const account = useAccount();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { mutateAsync } = useMutation({
|
||||
mutationFn: async (update: Partial<User["settings"]>) =>
|
||||
await queryFn({
|
||||
path: ["auth", "me"],
|
||||
method: "PATCH",
|
||||
body: { settings: { ...account!.settings, ...update } },
|
||||
path: ["auth", "users", "me"],
|
||||
compute: (update: Partial<User["claims"]["settings"]>) => ({
|
||||
body: {
|
||||
claims: {
|
||||
settings: { ...account!.claims.settings, ...update },
|
||||
},
|
||||
},
|
||||
}),
|
||||
onMutate: async (newSettings) => {
|
||||
const next = { ...account!, settings: { ...account!.settings, ...newSettings } };
|
||||
await queryClient.cancelQueries({ queryKey: ["auth", "me"] });
|
||||
const previous = queryClient.getQueryData(["auth", "me"]);
|
||||
queryClient.setQueryData(["auth", "me"], next);
|
||||
return { previous, next };
|
||||
optimistic: (update) => ({
|
||||
...account,
|
||||
claims: {
|
||||
...account!.claims,
|
||||
settings: { ...account!.claims.settings, ...update },
|
||||
},
|
||||
onError: (_, __, context) => {
|
||||
queryClient.setQueryData(["auth", "me"], context!.previous);
|
||||
},
|
||||
onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["auth", "me"] }),
|
||||
}),
|
||||
invalidate: ["auth", "users", "me"],
|
||||
});
|
||||
|
||||
if (!account) return null;
|
||||
return [
|
||||
account.settings[setting],
|
||||
async (value: User["settings"][Setting]) => {
|
||||
account.claims.settings[setting],
|
||||
async (value: User["claims"]["settings"][Setting]) => {
|
||||
await mutateAsync({ [setting]: value });
|
||||
},
|
||||
] as const;
|
||||
72
front/src/ui/settings/general.tsx
Normal file
72
front/src/ui/settings/general.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
// import Theme from "@material-symbols/svg-400/outlined/dark_mode.svg";
|
||||
import Language from "@material-symbols/svg-400/outlined/language.svg";
|
||||
import Android from "@material-symbols/svg-400/rounded/android.svg";
|
||||
import Public from "@material-symbols/svg-400/rounded/public.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, Select } from "~/primitives";
|
||||
import { supportedLanguages } from "~/providers/translations.compile";
|
||||
import { useLanguageName } from "~/track-utils";
|
||||
import { Preference, SettingsContainer } from "./base";
|
||||
|
||||
export const GeneralSettings = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
// const theme = useUserTheme("auto");
|
||||
const getLanguageName = useLanguageName();
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.general.label")}>
|
||||
{/* <Preference */}
|
||||
{/* icon={Theme} */}
|
||||
{/* label={t("settings.general.theme.label")} */}
|
||||
{/* description={t("settings.general.theme.description")} */}
|
||||
{/* > */}
|
||||
{/* <Select */}
|
||||
{/* label={t("settings.general.theme.label")} */}
|
||||
{/* value={theme} */}
|
||||
{/* onValueChange={(value) => setUserTheme(value)} */}
|
||||
{/* values={["auto", "light", "dark"]} */}
|
||||
{/* getLabel={(key) => t(`settings.general.theme.${key}`)} */}
|
||||
{/* /> */}
|
||||
{/* </Preference> */}
|
||||
<Preference
|
||||
icon={Language}
|
||||
label={t("settings.general.language.label")}
|
||||
description={t("settings.general.language.description")}
|
||||
>
|
||||
<Select
|
||||
label={t("settings.general.language.label")}
|
||||
value={i18n.resolvedLanguage!}
|
||||
onValueChange={(value) => i18n.changeLanguage(value)}
|
||||
values={supportedLanguages}
|
||||
getLabel={(key) => getLanguageName(key) ?? key}
|
||||
/>
|
||||
</Preference>
|
||||
</SettingsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const About = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.about.label")}>
|
||||
<Link
|
||||
href="https://github.com/zoriya/kyoo/releases/latest/download/kyoo.apk"
|
||||
target="_blank"
|
||||
>
|
||||
<Preference
|
||||
icon={Android}
|
||||
label={t("settings.about.android-app.label")}
|
||||
description={t("settings.about.android-app.description")}
|
||||
/>
|
||||
</Link>
|
||||
<Link href="https://github.com/zoriya/kyoo" target="_blank">
|
||||
<Preference
|
||||
icon={Public}
|
||||
label={t("settings.about.git.label")}
|
||||
description={t("settings.about.git.description")}
|
||||
/>
|
||||
</Link>
|
||||
</SettingsContainer>
|
||||
);
|
||||
};
|
||||
20
front/src/ui/settings/index.tsx
Normal file
20
front/src/ui/settings/index.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { ScrollView } from "react-native";
|
||||
import { ts } from "~/primitives";
|
||||
import { useAccount } from "~/providers/account-context";
|
||||
import { AccountSettings } from "./account";
|
||||
import { About, GeneralSettings } from "./general";
|
||||
// import { OidcSettings } from "./oidc";
|
||||
import { PlaybackSettings } from "./playback";
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const account = useAccount();
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ gap: ts(4), paddingBottom: ts(4) }}>
|
||||
<GeneralSettings />
|
||||
{account && <PlaybackSettings />}
|
||||
{account && <AccountSettings />}
|
||||
{/* {account && <OidcSettings />} */}
|
||||
<About />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
108
front/src/ui/settings/oidc.tsx
Normal file
108
front/src/ui/settings/oidc.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
// import {
|
||||
// type QueryIdentifier,
|
||||
// type ServerInfo,
|
||||
// ServerInfoP,
|
||||
// queryFn,
|
||||
// useAccount,
|
||||
// useFetch,
|
||||
// } from "@kyoo/models";
|
||||
// import { Button, IconButton, Link, Skeleton, tooltip, ts } from "@kyoo/primitives";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
// import { ImageBackground } from "react-native";
|
||||
// import { rem, useYoshiki } from "yoshiki/native";
|
||||
// import { ErrorView } from "../errors";
|
||||
// import { Preference, SettingsContainer } from "./base";
|
||||
//
|
||||
// import Badge from "@material-symbols/svg-400/outlined/badge.svg";
|
||||
// import Remove from "@material-symbols/svg-400/outlined/close.svg";
|
||||
// import OpenProfile from "@material-symbols/svg-400/outlined/open_in_new.svg";
|
||||
// import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
//
|
||||
// export const OidcSettings = () => {
|
||||
// const account = useAccount()!;
|
||||
// const { css } = useYoshiki();
|
||||
// const { t } = useTranslation();
|
||||
// const { data, error } = useFetch(OidcSettings.query());
|
||||
// const queryClient = useQueryClient();
|
||||
// const { mutateAsync: unlinkAccount } = useMutation({
|
||||
// mutationFn: async (provider: string) =>
|
||||
// await queryFn({
|
||||
// path: ["auth", "login", provider],
|
||||
// method: "DELETE",
|
||||
// }),
|
||||
// onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["auth", "me"] }),
|
||||
// });
|
||||
//
|
||||
// return (
|
||||
// <SettingsContainer title={t("settings.oidc.label")}>
|
||||
// {error ? (
|
||||
// <ErrorView error={error} />
|
||||
// ) : data ? (
|
||||
// Object.entries(data.oidc).map(([id, x]) => {
|
||||
// const acc = account.externalId[id];
|
||||
// return (
|
||||
// <Preference
|
||||
// key={x.displayName}
|
||||
// icon={Badge}
|
||||
// label={x.displayName}
|
||||
// description={
|
||||
// acc
|
||||
// ? t("settings.oidc.connected", { username: acc.username })
|
||||
// : t("settings.oidc.not-connected")
|
||||
// }
|
||||
// customIcon={
|
||||
// x.logoUrl != null && (
|
||||
// <ImageBackground
|
||||
// source={{ uri: x.logoUrl }}
|
||||
// {...css({ width: ts(3), height: ts(3), marginRight: ts(2) })}
|
||||
// />
|
||||
// )
|
||||
// }
|
||||
// >
|
||||
// {acc ? (
|
||||
// <>
|
||||
// {acc.profileUrl && (
|
||||
// <IconButton
|
||||
// icon={OpenProfile}
|
||||
// as={Link}
|
||||
// href={acc.profileUrl}
|
||||
// target="_blank"
|
||||
// {...tooltip(t("settings.oidc.open-profile", { provider: x.displayName }))}
|
||||
// />
|
||||
// )}
|
||||
// <IconButton
|
||||
// icon={Remove}
|
||||
// onPress={() => unlinkAccount(id)}
|
||||
// {...tooltip(t("settings.oidc.delete", { provider: x.displayName }))}
|
||||
// />
|
||||
// </>
|
||||
// ) : (
|
||||
// <Button
|
||||
// text={t("settings.oidc.link")}
|
||||
// as={Link}
|
||||
// href={x.link}
|
||||
// {...css({ minWidth: rem(6) })}
|
||||
// />
|
||||
// )}
|
||||
// </Preference>
|
||||
// );
|
||||
// })
|
||||
// ) : (
|
||||
// [...Array(3)].map((_, i) => (
|
||||
// <Preference
|
||||
// key={i}
|
||||
// customIcon={<Skeleton {...css({ width: ts(3), height: ts(3) })} />}
|
||||
// icon={null!}
|
||||
// label={<Skeleton {...css({ width: rem(6) })} />}
|
||||
// description={<Skeleton {...css({ width: rem(7), height: rem(0.8) })} />}
|
||||
// />
|
||||
// ))
|
||||
// )}
|
||||
// </SettingsContainer>
|
||||
// );
|
||||
// };
|
||||
//
|
||||
// OidcSettings.query = (): QueryIdentifier<ServerInfo> => ({
|
||||
// path: ["info"],
|
||||
// parser: ServerInfoP,
|
||||
// });
|
||||
@ -1,32 +1,26 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { languageCodes, useLanguageName } from "../utils";
|
||||
import { Preference, SettingsContainer, useSetting } from "./base";
|
||||
|
||||
import { useLocalSetting } from "@kyoo/models";
|
||||
import { Select } from "@kyoo/primitives";
|
||||
import SubtitleLanguage from "@material-symbols/svg-400/rounded/closed_caption-fill.svg";
|
||||
import PlayModeI from "@material-symbols/svg-400/rounded/display_settings-fill.svg";
|
||||
import AudioLanguage from "@material-symbols/svg-400/rounded/music_note-fill.svg";
|
||||
import langmap from "langmap";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Select } from "~/primitives";
|
||||
import { useLocalSetting } from "~/providers/settings";
|
||||
import { useLanguageName } from "~/track-utils";
|
||||
import { Preference, SettingsContainer, useSetting } from "./base";
|
||||
|
||||
const seenNativeNames = new Set();
|
||||
export const languageCodes = Object.keys(langmap)
|
||||
.filter((x) => {
|
||||
const nativeName = langmap[x]?.nativeName;
|
||||
|
||||
// Only include if nativeName is unique and defined
|
||||
if (nativeName && !seenNativeNames.has(nativeName)) {
|
||||
seenNativeNames.add(nativeName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.filter((x) => !x.includes("@"));
|
||||
|
||||
export const PlaybackSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -61,7 +55,9 @@ export const PlaybackSettings = () => {
|
||||
onValueChange={(value) => setAudio(value)}
|
||||
values={["default", ...languageCodes]}
|
||||
getLabel={(key) =>
|
||||
key === "default" ? t("mediainfo.default") : (getLanguageName(key) ?? key)
|
||||
key === "default"
|
||||
? t("mediainfo.default")
|
||||
: (getLanguageName(key) ?? key)
|
||||
}
|
||||
/>
|
||||
</Preference>
|
||||
@ -73,7 +69,9 @@ export const PlaybackSettings = () => {
|
||||
<Select
|
||||
label={t("settings.playback.subtitleLanguage.label")}
|
||||
value={subtitle ?? "none"}
|
||||
onValueChange={(value) => setSubtitle(value === "none" ? null : value)}
|
||||
onValueChange={(value) =>
|
||||
setSubtitle(value === "none" ? null : value)
|
||||
}
|
||||
values={["none", "default", ...languageCodes]}
|
||||
getLabel={(key) =>
|
||||
key === "none"
|
||||
@ -16,6 +16,7 @@
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react-jsx",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"types": [
|
||||
"node",
|
||||
"react"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user