diff --git a/src/assets/css/index.css b/src/assets/css/index.css index f32f26a9..74eb1270 100644 --- a/src/assets/css/index.css +++ b/src/assets/css/index.css @@ -4,7 +4,7 @@ html, body { - @apply bg-background-main font-open-sans text-type-text overflow-x-hidden; + @apply bg-background-main font-open-sans text-type-text; min-height: 100vh; min-height: 100dvh; } @@ -221,4 +221,4 @@ input[type=range].styled-slider.slider-progress::-ms-fill-lower { .tabbable:focus-visible { outline: 2px solid theme('colors.themePreview.primary'); box-shadow: 0 0 10px theme('colors.themePreview.secondary'); -} \ No newline at end of file +} diff --git a/src/assets/languages.ts b/src/assets/languages.ts index b05e1412..7fb52914 100644 --- a/src/assets/languages.ts +++ b/src/assets/languages.ts @@ -4,6 +4,7 @@ import en from "@/assets/locales/en.json"; import fr from "@/assets/locales/fr.json"; import it from "@/assets/locales/it.json"; import nl from "@/assets/locales/nl.json"; +import pirate from "@/assets/locales/pirate.json"; import pl from "@/assets/locales/pl.json"; import tr from "@/assets/locales/tr.json"; import vi from "@/assets/locales/vi.json"; @@ -20,4 +21,5 @@ export const locales = { tr, vi, zh, + pirate, }; diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 7f44b8d2..8951793d 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -1,387 +1,391 @@ { - "auth": { - "deviceNameLabel": "Device name", - "deviceNamePlaceholder": "Muad'Dib's Nintendo Switch", - "register": { - "information": { - "title": "Account information", - "color1": "First color", - "color2": "Second color", - "icon": "User icon", - "header": "Enter a name for your device and choose a user icon and colours" - } - }, - "login": { - "title": "Login to your account", - "description": "Oh, you're asking for the key to my top-secret lair, also known as The Fortress of Wordsmithery, accessed only by reciting the sacred incantation of the 12-word passphrase!", - "validationError": "Invalid or incomplete passphrase", - "submit": "Login", - "passphraseLabel": "12-Word Passphrase", - "passphrasePlaceholder": "Passphrase" - }, - "generate": { - "title": "Your passphrase", - "description": "If you lose this, you're a silly goose and will be posted on the wall of shame™️" - }, - "trust": { - "title": "Do you trust this host?", - "host": "Do you trust <0>{{hostname}}?", - "failed": { - "title": "Failed to reach backend", - "text": "Did you configure it correctly?" - }, - "yes": "Trust", - "no": "Go back" - }, - "verify": { - "title": "Enter your passphrase", - "description": "If you've already lost it, how will you ever be able to take care of a child?", - "invalidData": "Data is not valid", - "noMatch": "Passphrase doesn't match", - "recaptchaFailed": "ReCaptcha validation failed", - "passphraseLabel": "Your passphrase", - "register": "Register" - } + "auth": { + "deviceNameLabel": "Device name", + "deviceNamePlaceholder": "Muad'Dib's Nintendo Switch", + "register": { + "information": { + "title": "Account information", + "color1": "First color", + "color2": "Second color", + "icon": "User icon", + "header": "Enter a name for your device and choose a user icon and colours" + } }, - "errors": { - "details": "Error details", - "reloadPage": "Reload the page", - "badge": "It broke", - "title": "That's an error boss" + "login": { + "title": "Login to your account", + "description": "Oh, you're asking for the key to my top-secret lair, also known as The Fortress of Wordsmithery, accessed only by reciting the sacred incantation of the 12-word passphrase!", + "validationError": "Invalid or incomplete passphrase", + "submit": "Login", + "passphraseLabel": "12-Word Passphrase", + "passphrasePlaceholder": "Passphrase" }, - "notFound": { - "badge": "Not found", - "title": "Couldn't find that page", - "message": "We looked everywhere: under the bins, in the closet, behind the proxy but ultimately couldn't find the page you are looking for.", - "goHome": "Back to home" + "generate": { + "title": "Your passphrase", + "description": "If you lose this, you're a silly goose and will be posted on the wall of shame™️" }, - "global": { - "name": "movie-web" + "trust": { + "title": "Do you trust this host?", + "host": "Do you trust <0>{{hostname}}?", + "failed": { + "title": "Failed to reach backend", + "text": "Did you configure it correctly?" + }, + "yes": "Trust", + "no": "Go back" }, - "media": { - "types": { - "movie": "Movie", - "show": "Show" - }, - "episodeDisplay": "S{{season}} E{{episode}}" - }, - "player": { - "scraping": { - "notFound": { - "badge": "Not found", - "title": "Goo goo gaa gaa", - "text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖", - "homeButton": "Go home" - }, - "items": { - "pending": "Checking for videos...", - "notFound": "Doesn't have the video", - "failure": "Error occured" - } - }, - "playbackError": { - "badge": "Not found", - "title": "Whoops, it broke!", - "text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖", - "homeButton": "Go home", - "errors": { - "errorAborted": "The fetching of the associated resource was aborted by the user's request.", - "errorNetwork": "Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.", - "errorDecode": "Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.", - "errorNotSupported": "The associated resource or media provider object has been found to be unsuitable.", - "errorGenericMedia": "Unknown media error occured" - } - }, - "metadata": { - "notFound": { - "badge": "Not found", - "title": "Couldn't find that media.", - "text": "We couldn't find the media you requested. Either it's been removed or you tampered with the URL.", - "homeButton": "Back to home" - }, - "failed": { - "badge": "Failed", - "title": "Failed to load meta data", - "text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖", - "homeButton": "Go home" - } - }, - "back": { - "default": "Back to home", - "short": "Back" - }, - "time": { - "short": "-{{timeLeft}}", - "regular": "{{timeWatched}} / {{duration}}", - "remaining": "{{timeLeft}} left • Finish at {{timeFinished, datetime}}" - }, - "nextEpisode": { - "next": "Next episode", - "cancel": "Cancel" - }, - "menus": { - "settings": { - "videoSection": "Video settings", - "experienceSection": "Viewing Experience", - "enableCaptions": "Enable Captions", - "captionItem": "Caption settings", - "sourceItem": "Video sources", - "playbackItem": "Playback settings", - "downloadItem": "Download", - "qualityItem": "Quality" - }, - "episodes": { - "button": "Episodes", - "loadingTitle": "Loading...", - "loadingList": "Loading...", - "loadingError": "Error loading season", - "emptyState": "There are no episodes in this season, check back later!", - "episodeBadge": "E{{episode}}" - }, - "sources": { - "title": "Sources", - "unknownOption": "Unknown", - "noStream": { - "title": "No stream", - "text": "This source has no streams for this movie or show." - }, - "noEmbeds": { - "title": "No embeds found", - "text": "We were unable to find any embeds for this source, please try another." - }, - "failed": { - "title": "Failed to scrape", - "text": "We were unable to find any videos for this source. Don't come bitchin' to us about it, just try another source." - } - }, - "captions": { - "title": "Captions", - "customizeLabel": "Customize", - "settings": { - "fixCapitals": "Fix capitalization", - "delay": "Caption delay" - }, - "customChoice": "Upload captions", - "offChoice": "Off", - "unknownLanguage": "Unknown" - }, - "downloads": { - "title": "Download", - "disclaimer": "Downloads are taken directly from the provider. movie-web does not have control over how the downloads are provided.", - "hlsExplanation": "Insert explanation for why you can't download HLS here", - "downloadVideo": "Download video", - "downloadCaption": "Download current caption", - "onPc": { - "1": "On PC, right click the video and select Save video as", - "title": "Downloading on PC", - "shortTitle": "Download / PC" - }, - "onAndroid": { - "1": "To download on Android, tap and hold on the video, then select save.", - "title": "Downloading on Android", - "shortTitle": "Download / Android" - }, - "onIos": { - "1": "To download on iOS, click , then Save to Files . All that's left to do now is to pick a nice and cozy folder for your video!", - "title": "Downloading on iOS", - "shortTitle": "Download / iOS" - } - }, - "playback": { - "title": "Playback settings", - "speedLabel": "Playback speed" - }, - "quality": { - "title": "Quality", - "automaticLabel": "Automatic quality", - "hint": "You can try <0>switching source to get different quality options." - } - } - }, - "home": { - "mediaList": { - "stopEditing": "Stop editing" - }, - "titles": { - "morning": [ - "Morning title" - ], - "day": [ - "Day title" - ], - "night": [ - "Night title" - ] - }, - "search": { - "loading": "Loading...", - "sectionTitle": "Search results", - "allResults": "That's all we have!", - "noResults": "We couldn't find anything!", - "failed": "Failed to find media, try again!", - "placeholder": "What do you want to watch?" - }, - "continueWatching": { - "sectionTitle": "Continue Watching" - }, - "bookmarks": { - "sectionTitle": "Bookmarks" - } - }, - "overlays": { - "close": "Close" - }, - "screens": { - "loadingUser": "Loading your profile", - "loadingApp": "Loading application", - "loadingUserError": { - "text": "Failed to load your profile", - "textWithReset": "Failed to load your profile from your custom server, want to reset back to default?", - "reset": "Reset custom server" - }, - "migration": { - "failed": "Failed to migrate your data.", - "inProgress": "Please hold, we are migrating your data. This shouldn't take long." - }, - "dmca": { - "title": "DMCA", - "text": "In an effort to address the copyright concerns associated with the website known as \"movie-web,\" the DMCA, or Digital Millennium Copyright Act, has been initiated to safeguard the intellectual property rights of content creators by reporting infringements on this platform, thereby adhering to legal protocols for takedown requests, which, like, you know, it's all about, like, maintaining the integrity of intellectual property, and, um, making sure, like, creators get their fair share, but then, it's, like, this intricate dance of digital legalities, where you have to, uh, like, navigate this labyrinth of code and bytes and, uh, send, you know, these, like, electronic documents that, um, point out the, uh, alleged infringement, and it's, like, this whole, like, teeter-totter of legality, where you're, like, balancing, um, the rights of the, you know, creators and the, um, operation of this, like, online, uh, entity, and, like, the DMCA, it's, like, this, um, powerful tool, but, uh, it's also, like, this, um, complex puzzle, where, you know, you're, like, seeking justice in the digital wilderness, and, uh, striving for harmony amidst the chaos of the internet, and, um, yeah, that's, like, the whole, like, DMCA-ing thing with movie-web, you know?" - } - }, - "navigation": { - "banner": { - "offline": "Check your internet connection" - }, - "menu": { - "register": "Sync to cloud", - "settings": "Settings", - "about": "About us", - "support": "Support", - "logout": "Log out" - } - }, - "actions": { - "copy": "Copy", - "copied": "Copied", - "next": "Next" - }, - "settings": { - "unsaved": "You have unsaved changes", - "reset": "Reset", - "save": "Save", - "sidebar": { - "info": { - "title": "App information", - "hostname": "Hostname", - "backendUrl": "Backend URL", - "userId": "User ID", - "notLoggedIn": "Not logged in", - "appVersion": "App version", - "backendVersion": "Backend version", - "unknownVersion": "Unknown", - "secure": "Secure", - "insecure": "Insecure" - } - }, - "appearance": { - "title": "Appearance", - "activeTheme": "Active", - "themes": { - "default": "Default", - "blue": "Blue", - "teal": "Teal", - "red": "Red", - "gray": "Gray" - } - }, - "account": { - "title": "Account", - "register": { - "title": "Sync to the cloud", - "text": "Instantly share your watch progress between devices and keep them synced.", - "cta": "Get started" - }, - "profile": { - "title": "Edit profile picture", - "firstColor": "First color", - "secondColor": "Second color", - "userIcon": "User icon", - "finish": "Finish editing" - }, - "devices": { - "title": "Devices", - "failed": "Failed to load sessions", - "deviceNameLabel": "Device name", - "removeDevice": "Remove" - }, - "accountDetails": { - "editProfile": "Edit", - "deviceNameLabel": "Device name", - "deviceNamePlaceholder": "Fremen tablet", - "logoutButton": "Log out" - }, - "actions": { - "title": "Actions", - "delete": { - "title": "Delete account", - "text": "This action is irreversible. All data will be deleted and nothing can be recovered.", - "button": "Delete account", - "confirmTitle": "Are you sure?", - "confirmDescription": "Are you sure you want to delete your account? All your data will be lost!", - "confirmButton": "Delete account" - } - } - }, - "locale": { - "title": "Locale", - "language": "Application language", - "languageDescription": "Language applied to the entire application." - }, - "captions": { - "title": "Captions", - "previewQuote": "I must not fear. Fear is the mind-killer.", - "backgroundLabel": "Background opacity", - "textSizeLabel": "Text size", - "colorLabel": "Color" - }, - "connections": { - "title": "Connections", - "workers": { - "label": "Use custom proxy workers", - "description": "To make the application function, all traffic is routed through proxies. Enable this if you want to bring your own workers.", - "urlLabel": "Worker URLs", - "emptyState": "No workers yet, add one below", - "urlPlaceholder": "https://", - "addButton": "Add new worker" - }, - "server": { - "label": "Custom server", - "description": "To make the application function, all traffic is routed through proxies. Enable this if you want to bring your own workers.", - "urlLabel": "Custom server URL" - } - } - }, - "faq": { - "title": "About us", - "q1": { - "title": "1", - "body": "Body of 1" - }, - "how": { - "title": "1", - "body": "Body of 1" - } - }, - "footer": { - "tagline": "Watch your favorite shows and movies with this open source streaming app.", - "links": { - "github": "GitHub", - "dmca": "DMCA", - "discord": "Discord" - }, - "legal": { - "disclaimer": "Disclaimer", - "disclaimerText": "movie-web does not host any files, it merely links to 3rd party services. Legal issues should be taken up with the file hosts and providers. movie-web is not responsible for any media files shown by the video providers." - } + "verify": { + "title": "Enter your passphrase", + "description": "If you've already lost it, how will you ever be able to take care of a child?", + "invalidData": "Data is not valid", + "noMatch": "Passphrase doesn't match", + "recaptchaFailed": "ReCaptcha validation failed", + "passphraseLabel": "Your passphrase", + "register": "Register" } + }, + "errors": { + "details": "Error details", + "reloadPage": "Reload the page", + "badge": "It broke", + "title": "That's an error boss" + }, + "notFound": { + "badge": "Not found", + "title": "Couldn't find that page", + "message": "We looked everywhere: under the bins, in the closet, behind the proxy but ultimately couldn't find the page you are looking for.", + "goHome": "Back to home" + }, + "global": { + "name": "movie-web", + "pages": { + "pagetitle": "{{title}} - movie-web", + "dmca": "DMCA", + "settings": "Settings", + "about": "About", + "login": "Login", + "register": "Register" + } + }, + "media": { + "types": { + "movie": "Movie", + "show": "Show" + }, + "episodeDisplay": "S{{season}} E{{episode}}" + }, + "player": { + "scraping": { + "notFound": { + "badge": "Not found", + "title": "Goo goo gaa gaa", + "text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖", + "homeButton": "Go home" + }, + "items": { + "pending": "Checking for videos...", + "notFound": "Doesn't have the video", + "failure": "Error occured" + } + }, + "playbackError": { + "badge": "Not found", + "title": "Whoops, it broke!", + "text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖", + "homeButton": "Go home", + "errors": { + "errorAborted": "The fetching of the associated resource was aborted by the user's request.", + "errorNetwork": "Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.", + "errorDecode": "Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.", + "errorNotSupported": "The associated resource or media provider object has been found to be unsuitable.", + "errorGenericMedia": "Unknown media error occured" + } + }, + "metadata": { + "notFound": { + "badge": "Not found", + "title": "Couldn't find that media.", + "text": "We couldn't find the media you requested. Either it's been removed or you tampered with the URL.", + "homeButton": "Back to home" + }, + "failed": { + "badge": "Failed", + "title": "Failed to load meta data", + "text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖", + "homeButton": "Go home" + } + }, + "back": { + "default": "Back to home", + "short": "Back" + }, + "time": { + "regular": "{{timeWatched}} / {{duration}}", + "shortRegular": "{{timeWatched}}", + "remaining": "{{timeLeft}} left • Finish at {{timeFinished, datetime}}", + "shortRemaining": "-{{timeLeft}}" + }, + "nextEpisode": { + "next": "Next episode", + "cancel": "Cancel" + }, + "menus": { + "settings": { + "videoSection": "Video settings", + "experienceSection": "Viewing Experience", + "enableCaptions": "Enable Captions", + "captionItem": "Caption settings", + "sourceItem": "Video sources", + "playbackItem": "Playback settings", + "downloadItem": "Download", + "qualityItem": "Quality" + }, + "episodes": { + "button": "Episodes", + "loadingTitle": "Loading...", + "loadingList": "Loading...", + "loadingError": "Error loading season", + "emptyState": "There are no episodes in this season, check back later!", + "episodeBadge": "E{{episode}}" + }, + "sources": { + "title": "Sources", + "unknownOption": "Unknown", + "noStream": { + "title": "No stream", + "text": "This source has no streams for this movie or show." + }, + "noEmbeds": { + "title": "No embeds found", + "text": "We were unable to find any embeds for this source, please try another." + }, + "failed": { + "title": "Failed to scrape", + "text": "We were unable to find any videos for this source. Don't come bitchin' to us about it, just try another source." + } + }, + "captions": { + "title": "Captions", + "customizeLabel": "Customize", + "settings": { + "fixCapitals": "Fix capitalization", + "delay": "Caption delay" + }, + "customChoice": "Upload captions", + "offChoice": "Off", + "unknownLanguage": "Unknown" + }, + "downloads": { + "title": "Download", + "disclaimer": "Downloads are taken directly from the provider. movie-web does not have control over how the downloads are provided.", + "hlsExplanation": "Insert explanation for why you can't download HLS here", + "downloadVideo": "Download video", + "downloadCaption": "Download current caption", + "onPc": { + "1": "On PC, right click the video and select Save video as", + "title": "Downloading on PC", + "shortTitle": "Download / PC" + }, + "onAndroid": { + "1": "To download on Android, tap and hold on the video, then select save.", + "title": "Downloading on Android", + "shortTitle": "Download / Android" + }, + "onIos": { + "1": "To download on iOS, click , then Save to Files . All that's left to do now is to pick a nice and cozy folder for your video!", + "title": "Downloading on iOS", + "shortTitle": "Download / iOS" + } + }, + "playback": { + "title": "Playback settings", + "speedLabel": "Playback speed" + }, + "quality": { + "title": "Quality", + "automaticLabel": "Automatic quality", + "hint": "You can try <0>switching source to get different quality options.", + "iosNoQuality": "Due to Apple-defined limitations, quality selection is not available on iOS for this source. You can try <0>switching to another source to get different quality options." + } + } + }, + "home": { + "mediaList": { + "stopEditing": "Stop editing" + }, + "titles": { + "morning": ["Morning title"], + "day": ["Day title"], + "night": ["Night title"] + }, + "search": { + "loading": "Loading...", + "sectionTitle": "Search results", + "allResults": "That's all we have!", + "noResults": "We couldn't find anything!", + "failed": "Failed to find media, try again!", + "placeholder": "What do you want to watch?" + }, + "continueWatching": { + "sectionTitle": "Continue Watching" + }, + "bookmarks": { + "sectionTitle": "Bookmarks" + } + }, + "overlays": { + "close": "Close" + }, + "screens": { + "loadingUser": "Loading your profile", + "loadingApp": "Loading application", + "loadingUserError": { + "text": "Failed to load your profile", + "textWithReset": "Failed to load your profile from your custom server, want to reset back to default?", + "reset": "Reset custom server" + }, + "migration": { + "failed": "Failed to migrate your data.", + "inProgress": "Please hold, we are migrating your data. This shouldn't take long." + }, + "dmca": { + "title": "DMCA", + "text": "In an effort to address the copyright concerns associated with the website known as \"movie-web,\" the DMCA, or Digital Millennium Copyright Act, has been initiated to safeguard the intellectual property rights of content creators by reporting infringements on this platform, thereby adhering to legal protocols for takedown requests, which, like, you know, it's all about, like, maintaining the integrity of intellectual property, and, um, making sure, like, creators get their fair share, but then, it's, like, this intricate dance of digital legalities, where you have to, uh, like, navigate this labyrinth of code and bytes and, uh, send, you know, these, like, electronic documents that, um, point out the, uh, alleged infringement, and it's, like, this whole, like, teeter-totter of legality, where you're, like, balancing, um, the rights of the, you know, creators and the, um, operation of this, like, online, uh, entity, and, like, the DMCA, it's, like, this, um, powerful tool, but, uh, it's also, like, this, um, complex puzzle, where, you know, you're, like, seeking justice in the digital wilderness, and, uh, striving for harmony amidst the chaos of the internet, and, um, yeah, that's, like, the whole, like, DMCA-ing thing with movie-web, you know?" + } + }, + "navigation": { + "banner": { + "offline": "Check your internet connection" + }, + "menu": { + "register": "Sync to cloud", + "settings": "Settings", + "about": "About us", + "support": "Support", + "logout": "Log out" + } + }, + "actions": { + "copy": "Copy", + "copied": "Copied", + "next": "Next" + }, + "settings": { + "unsaved": "You have unsaved changes", + "reset": "Reset", + "save": "Save", + "sidebar": { + "info": { + "title": "App information", + "hostname": "Hostname", + "backendUrl": "Backend URL", + "userId": "User ID", + "notLoggedIn": "Not logged in", + "appVersion": "App version", + "backendVersion": "Backend version", + "unknownVersion": "Unknown", + "secure": "Secure", + "insecure": "Insecure" + } + }, + "appearance": { + "title": "Appearance", + "activeTheme": "Active", + "themes": { + "default": "Default", + "blue": "Blue", + "teal": "Teal", + "red": "Red", + "gray": "Gray" + } + }, + "account": { + "title": "Account", + "register": { + "title": "Sync to the cloud", + "text": "Instantly share your watch progress between devices and keep them synced.", + "cta": "Get started" + }, + "profile": { + "title": "Edit profile picture", + "firstColor": "First color", + "secondColor": "Second color", + "userIcon": "User icon", + "finish": "Finish editing" + }, + "devices": { + "title": "Devices", + "failed": "Failed to load sessions", + "deviceNameLabel": "Device name", + "removeDevice": "Remove" + }, + "accountDetails": { + "editProfile": "Edit", + "deviceNameLabel": "Device name", + "deviceNamePlaceholder": "Fremen tablet", + "logoutButton": "Log out" + }, + "actions": { + "title": "Actions", + "delete": { + "title": "Delete account", + "text": "This action is irreversible. All data will be deleted and nothing can be recovered.", + "button": "Delete account", + "confirmTitle": "Are you sure?", + "confirmDescription": "Are you sure you want to delete your account? All your data will be lost!", + "confirmButton": "Delete account" + } + } + }, + "locale": { + "title": "Locale", + "language": "Application language", + "languageDescription": "Language applied to the entire application." + }, + "captions": { + "title": "Captions", + "previewQuote": "I must not fear. Fear is the mind-killer.", + "backgroundLabel": "Background opacity", + "textSizeLabel": "Text size", + "colorLabel": "Color" + }, + "connections": { + "title": "Connections", + "workers": { + "label": "Use custom proxy workers", + "description": "To make the application function, all traffic is routed through proxies. Enable this if you want to bring your own workers.", + "urlLabel": "Worker URLs", + "emptyState": "No workers yet, add one below", + "urlPlaceholder": "https://", + "addButton": "Add new worker" + }, + "server": { + "label": "Custom server", + "description": "To make the application function, all traffic is routed through proxies. Enable this if you want to bring your own workers.", + "urlLabel": "Custom server URL" + } + } + }, + "faq": { + "title": "About us", + "q1": { + "title": "1", + "body": "Body of 1" + }, + "how": { + "title": "1", + "body": "Body of 1" + } + }, + "footer": { + "tagline": "Watch your favorite shows and movies with this open source streaming app.", + "links": { + "github": "GitHub", + "dmca": "DMCA", + "discord": "Discord" + }, + "legal": { + "disclaimer": "Disclaimer", + "disclaimerText": "movie-web does not host any files, it merely links to 3rd party services. Legal issues should be taken up with the file hosts and providers. movie-web is not responsible for any media files shown by the video providers." + } + } } diff --git a/src/assets/locales/pirate.json b/src/assets/locales/pirate.json new file mode 100644 index 00000000..5f66f526 --- /dev/null +++ b/src/assets/locales/pirate.json @@ -0,0 +1,391 @@ +{ + "auth": { + "deviceNameLabel": "Ship name", + "deviceNamePlaceholder": "Muad'Dib's Pirate Ship", + "register": { + "information": { + "title": "Pirate Account information", + "color1": "First Mate color", + "color2": "Second Mate color", + "icon": "Pirate icon", + "header": "Enter a moniker for yer ship and choose a pirate icon and colors, arrr!" + } + }, + "login": { + "title": "Hoist the Jolly Roger", + "description": "Arr, ye be askin' for the key to me top-secret lair, also known as The Fortress of Wordsmithery, accessed only by recitin' the sacred incantation of the 12-word passphrase!", + "validationError": "Arr, invalid or incomplete passphrase", + "submit": "Hoist Anchor", + "passphraseLabel": "12-Word Passphrase", + "passphrasePlaceholder": "Passphrase" + }, + "generate": { + "title": "Yer Passphrase", + "description": "If ye lose this, ye be a silly goose and will be posted on the wall of shame™️" + }, + "trust": { + "title": "Do ye trust this ship?", + "host": "Do ye trust <0>{{hostname}}?", + "failed": { + "title": "Failed to reach the backend", + "text": "Did ye configure it correctly?" + }, + "yes": "Trust", + "no": "Abandon Ship" + }, + "verify": { + "title": "Enter yer passphrase", + "description": "If ye already lost it, how will ye ever be able to take care of a wee buccaneer?", + "invalidData": "Data be not valid", + "noMatch": "Passphrase doesn't match", + "recaptchaFailed": "ReCaptcha validation failed", + "passphraseLabel": "Yer passphrase", + "register": "Register" + } + }, + "errors": { + "details": "Error details", + "reloadPage": "Reload the page", + "badge": "Shiver me timbers", + "title": "That be an error, Captain" + }, + "notFound": { + "badge": "Not found", + "title": "Couldn't find that treasure map", + "message": "We looked everywhere: under the bins, in the closet, behind the proxy but ultimately couldn't find the treasure map ye be lookin' for.", + "goHome": "Back to home port" + }, + "global": { + "name": "movie-web", + "pages": { + "pagetitle": "{{title}} - movie-web", + "dmca": "DMCA", + "settings": "Settings", + "about": "About", + "login": "Login", + "register": "Register" + } + }, + "media": { + "types": { + "movie": "Film", + "show": "Show" + }, + "episodeDisplay": "S{{season}} E{{episode}}" + }, + "player": { + "scraping": { + "notFound": { + "badge": "Not found", + "title": "Goo goo gaa gaa", + "text": "Oh, me apologies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can ye find it in yer heart to forgive? UwU 💖", + "homeButton": "Go home port" + }, + "items": { + "pending": "Checkin' for videos...", + "notFound": "Doesn't have the video", + "failure": "Error occurred" + } + }, + "playbackError": { + "badge": "Not found", + "title": "Whoops, it broke!", + "text": "Oh, me apologies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can ye find it in yer heart to forgive? UwU 💖", + "homeButton": "Go home port", + "errors": { + "errorAborted": "The fetchin' of the associated resource was aborted by the user's request.", + "errorNetwork": "Some kind of network error occurred which prevented the media from bein' successfully fetched, despite havin' previously been available.", + "errorDecode": "Despite havin' previously been determined to be usable, an error occurred while tryin' to decode the media resource, resultin' in an error.", + "errorNotSupported": "The associated resource or media provider object has been found to be unsuitable.", + "errorGenericMedia": "Unknown media error occurred" + } + }, + "metadata": { + "notFound": { + "badge": "Not found", + "title": "Couldn't find that media.", + "text": "We couldn't find the media ye requested. Either it's been removed or ye tampered with the URL.", + "homeButton": "Back to home port" + }, + "failed": { + "badge": "Failed", + "title": "Failed to load meta data", + "text": "Oh, me apologies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can ye find it in yer heart to forgive? UwU 💖", + "homeButton": "Go home port" + } + }, + "back": { + "default": "Back to home port", + "short": "Back" + }, + "time": { + "regular": "{{timeWatched}} / {{duration}}", + "shortRegular": "{{timeWatched}}", + "remaining": "{{timeLeft}} left • Finish at {{timeFinished, datetime}}", + "shortRemaining": "-{{timeLeft}}" + }, + "nextEpisode": { + "next": "Next episode", + "cancel": "Cancel" + }, + "menus": { + "settings": { + "videoSection": "Video settings", + "experienceSection": "Viewing Experience", + "enableCaptions": "Enable Sea Shanties", + "captionItem": "Sea Shanty settings", + "sourceItem": "Video sources", + "playbackItem": "Playback settings", + "downloadItem": "Buried Treasure", + "qualityItem": "Quality" + }, + "episodes": { + "button": "Episodes", + "loadingTitle": "Loading...", + "loadingList": "Loading...", + "loadingError": "Error loadin' season", + "emptyState": "There be no episodes in this season, check back later!", + "episodeBadge": "E{{episode}}" + }, + "sources": { + "title": "Sources", + "unknownOption": "Unknown", + "noStream": { + "title": "No stream", + "text": "This source has no streams for this film or show." + }, + "noEmbeds": { + "title": "No embeds found", + "text": "We were unable to find any embeds for this source, please try another." + }, + "failed": { + "title": "Failed to scrape", + "text": "We were unable to find any videos for this source. Don't come bitchin' to us about it, just try another source." + } + }, + "captions": { + "title": "Sea Shanties", + "customizeLabel": "Customize", + "settings": { + "fixCapitals": "Fix capitalization", + "delay": "Shanty delay" + }, + "customChoice": "Upload sea shanties", + "offChoice": "Off", + "unknownLanguage": "Unknown" + }, + "downloads": { + "title": "Buried Treasure", + "disclaimer": "Downloads be taken directly from the provider. movie-web does not have control over how the downloads be provided.", + "hlsExplanation": "Insert explanation for why ye can't download HLS here", + "downloadVideo": "Download film", + "downloadCaption": "Download current sea shanty", + "onPc": { + "1": "On PC, right click the film and select Save film as", + "title": "Downloadin' on PC", + "shortTitle": "Download / PC" + }, + "onAndroid": { + "1": "To download on Android, tap and hold on the film, then select save.", + "title": "Downloadin' on Android", + "shortTitle": "Download / Android" + }, + "onIos": { + "1": "To download on iOS, click , then Save to Files . All that's left to do now be to pick a nice and cozy chest for yer film!", + "title": "Downloadin' on iOS", + "shortTitle": "Download / iOS" + } + }, + "playback": { + "title": "Playback settings", + "speedLabel": "Playback speed" + }, + "quality": { + "title": "Quality", + "automaticLabel": "Automatic quality", + "hint": "Ye can try <0>switchin' source to get different quality options.", + "iosNoQuality": "Due to Apple-defined limitations, quality selection be not available on iOS for this source. Ye can try <0>switchin' to another source to get different quality options." + } + } + }, + "home": { + "mediaList": { + "stopEditing": "Stop editin'" + }, + "titles": { + "morning": ["Morning title"], + "day": ["Day title"], + "night": ["Night title"] + }, + "search": { + "loading": "Loading...", + "sectionTitle": "Searchin' results", + "allResults": "That's all we have, me heartie!", + "noResults": "We couldn't find anythin', arrr!", + "failed": "Failed to find media, try again!", + "placeholder": "What do ye want to watch?" + }, + "continueWatching": { + "sectionTitle": "Continue Watchin'" + }, + "bookmarks": { + "sectionTitle": "Bookmarks" + } + }, + "overlays": { + "close": "Close" + }, + "screens": { + "loadingUser": "Loadin' yer pirate profile", + "loadingApp": "Loadin' application", + "loadingUserError": { + "text": "Failed to load yer pirate profile", + "textWithReset": "Failed to load yer pirate profile from yer custom ship, want to reset back to default?", + "reset": "Reset custom ship" + }, + "migration": { + "failed": "Failed to migrate yer booty.", + "inProgress": "Please hold, we be migratin' yer booty. This shouldn't take long." + }, + "dmca": { + "title": "DMCA", + "text": "In an effort to address the copyright concerns associated with the website known as \"movie-web,\" the DMCA, or Digital Jolly Roger Copyright Act, has been initiated to safeguard the intellectual property rights of content creators by reportin' infringements on this platform, thereby adherin' to legal protocols for takedown requests, which, like, ye know, it's all about, like, maintainin' the integrity of intellectual property, and, um, makin' sure, like, creators get their fair share, but then, it's, like, this intricate dance of digital legalities, where ye have to, uh, like, navigate this labyrinth of code and bytes and, uh, send, ye know, these, like, electronic documents that, um, point out the, uh, alleged infringement, and it's, like, this whole, like, teeter-totter of legality, where ye're, like, balancin', um, the rights of the, ye know, creators and the, um, operation of this, like, online, uh, entity, and, like, the DMCA, it's, like, this, um, powerful tool, but, uh, it's also, like, this, um, complex puzzle, where, ye know, ye're, like, seekin' justice in the digital wilderness, and, uh, strivin' for harmony amidst the chaos of the internet, and, um, yeah, that's, like, the whole, like, DMCA-ing thing with movie-web, ye know?" + } + }, + "navigation": { + "banner": { + "offline": "Check yer internet connection" + }, + "menu": { + "register": "Sync to the cloud", + "settings": "Settings", + "about": "About us", + "support": "Support", + "logout": "Abandon ship" + } + }, + "actions": { + "copy": "Copy", + "copied": "Copied", + "next": "Next" + }, + "settings": { + "unsaved": "Ye have unsaved changes", + "reset": "Reset", + "save": "Save", + "sidebar": { + "info": { + "title": "App information", + "hostname": "Ship name", + "backendUrl": "Backend URL", + "userId": "Pirate ID", + "notLoggedIn": "Not logged in", + "appVersion": "App version", + "backendVersion": "Backend version", + "unknownVersion": "Unknown", + "secure": "Secure", + "insecure": "Insecure" + } + }, + "appearance": { + "title": "Appearance", + "activeTheme": "Active", + "themes": { + "default": "Default", + "blue": "Blue", + "teal": "Teal", + "red": "Red", + "gray": "Gray" + } + }, + "account": { + "title": "Treasure Chest", + "register": { + "title": "Sync to the Cloud", + "text": "Instantly share yer watch progress between devices and keep 'em synced.", + "cta": "Get started" + }, + "profile": { + "title": "Edit Pirate Portrait", + "firstColor": "First color", + "secondColor": "Second color", + "userIcon": "Pirate icon", + "finish": "Finish editing" + }, + "devices": { + "title": "Shipmates", + "failed": "Failed to load sessions", + "deviceNameLabel": "Ship name", + "removeDevice": "Abandon ship" + }, + "accountDetails": { + "editProfile": "Edit", + "deviceNameLabel": "Ship name", + "deviceNamePlaceholder": "Fremen tablet", + "logoutButton": "Abandon ship" + }, + "actions": { + "title": "Actions", + "delete": { + "title": "Abandon Account", + "text": "This action be irreversible. All booty will be deleted and nothin' can be recovered.", + "button": "Abandon Account", + "confirmTitle": "Arrr ye sure?", + "confirmDescription": "Arrr ye sure ye want to abandon yer account? All yer booty will be lost!", + "confirmButton": "Abandon Account" + } + } + }, + "locale": { + "title": "Locale", + "language": "Application language", + "languageDescription": "Language applied to the entire application." + }, + "captions": { + "title": "Captions", + "previewQuote": "I must not fear. Fear be the mind-killer.", + "backgroundLabel": "Background opacity", + "textSizeLabel": "Text size", + "colorLabel": "Color" + }, + "connections": { + "title": "Connections", + "workers": { + "label": "Use custom proxy sailors", + "description": "To make the application function, all traffic be routed through proxies. Enable this if ye want to bring yer own sailors.", + "urlLabel": "Sailor URLs", + "emptyState": "No sailors yet, add one below", + "urlPlaceholder": "https://", + "addButton": "Recruit new sailor" + }, + "server": { + "label": "Custom ship", + "description": "To make the application function, all traffic be routed through proxies. Enable this if ye want to bring yer own sailors.", + "urlLabel": "Custom ship URL" + } + } + }, + "faq": { + "title": "About us", + "q1": { + "title": "1", + "body": "Body of 1" + }, + "how": { + "title": "1", + "body": "Body of 1" + } + }, + "footer": { + "tagline": "Watch yer favorite shows and movies with this open source streaming ship.", + "links": { + "github": "GitHub", + "dmca": "DMCA", + "discord": "Discord" + }, + "legal": { + "disclaimer": "Disclaimer", + "disclaimerText": "movie-web does not host any files, it merely links to 3rd party services. Legal issues should be taken up with the file hosts and providers. movie-web be not responsible for any media files shown by the video providers." + } + } +} diff --git a/src/components/buttons/EditButton.tsx b/src/components/buttons/EditButton.tsx index 7c806968..793fb7c3 100644 --- a/src/components/buttons/EditButton.tsx +++ b/src/components/buttons/EditButton.tsx @@ -26,7 +26,7 @@ export function EditButton(props: EditButtonProps) { {props.editing ? ( - {t("media.stopEditing")} + {t("home.mediaList.stopEditing")} ) : ( diff --git a/src/components/form/SearchBar.tsx b/src/components/form/SearchBar.tsx index 26b09c8a..0573a285 100644 --- a/src/components/form/SearchBar.tsx +++ b/src/components/form/SearchBar.tsx @@ -1,5 +1,5 @@ import c from "classnames"; -import { useState } from "react"; +import { forwardRef, useState } from "react"; import { Flare } from "@/components/utils/Flare"; @@ -13,50 +13,53 @@ export interface SearchBarProps { value: string; } -export function SearchBarInput(props: SearchBarProps) { - const [focused, setFocused] = useState(false); +export const SearchBarInput = forwardRef( + (props, ref) => { + const [focused, setFocused] = useState(false); - function setSearch(value: string) { - props.onChange(value, false); - } + function setSearch(value: string) { + props.onChange(value, false); + } - return ( - - - - -
- -
- - { - setFocused(false); - props.onUnFocus(); - }} - onFocus={() => setFocused(true)} - onChange={(val) => setSearch(val)} - value={props.value} - className="w-full flex-1 bg-transparent px-4 py-4 pl-12 text-search-text placeholder-search-placeholder focus:outline-none sm:py-4 sm:pr-2" - placeholder={props.placeholder} + > + -
-
- ); -} + + +
+ +
+ + { + setFocused(false); + props.onUnFocus(); + }} + onFocus={() => setFocused(true)} + onChange={(val) => setSearch(val)} + value={props.value} + className="w-full flex-1 bg-transparent px-4 py-4 pl-12 text-search-text placeholder-search-placeholder focus:outline-none sm:py-4 sm:pr-2" + placeholder={props.placeholder} + /> +
+ + ); + } +); diff --git a/src/components/overlays/OverlayPage.tsx b/src/components/overlays/OverlayPage.tsx index 089b68cc..a5000ba8 100644 --- a/src/components/overlays/OverlayPage.tsx +++ b/src/components/overlays/OverlayPage.tsx @@ -50,7 +50,6 @@ export function OverlayPage(props: Props) { className={classNames([props.className, ""])} style={{ height: props.height ? `${props.height}px` : undefined, - maxHeight: "70vh", width: props.width ? width : undefined, }} > diff --git a/src/components/overlays/positions/OverlayMobilePosition.tsx b/src/components/overlays/positions/OverlayMobilePosition.tsx index 9c452957..df806146 100644 --- a/src/components/overlays/positions/OverlayMobilePosition.tsx +++ b/src/components/overlays/positions/OverlayMobilePosition.tsx @@ -16,7 +16,7 @@ export function OverlayMobilePosition(props: MobilePositionProps) { return (
diff --git a/src/components/player/atoms/Time.tsx b/src/components/player/atoms/Time.tsx index c750ef62..bb57997b 100644 --- a/src/components/player/atoms/Time.tsx +++ b/src/components/player/atoms/Time.tsx @@ -40,10 +40,14 @@ export function Time(props: { short?: boolean }) { const timeFinished = new Date(Date.now() + secondsRemaining * 1e3); const duration = formatSeconds(timeDuration, hasHours); - let localizationKey = "regular"; - if (props.short) localizationKey = "short"; - else if (timeFormat === VideoPlayerTimeFormat.REMAINING) - localizationKey = "remaining"; + let localizationKey = + timeFormat === VideoPlayerTimeFormat.REGULAR ? "regular" : "remaining"; + if (props.short) { + localizationKey = + timeFormat === VideoPlayerTimeFormat.REGULAR + ? "shortRegular" + : "shortRemaining"; + } return ( toggleMode()}> diff --git a/src/components/player/atoms/settings/QualityView.tsx b/src/components/player/atoms/settings/QualityView.tsx index f3d048e0..3b311bca 100644 --- a/src/components/player/atoms/settings/QualityView.tsx +++ b/src/components/player/atoms/settings/QualityView.tsx @@ -1,5 +1,6 @@ +import Hls from "hls.js"; import { t } from "i18next"; -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { Trans } from "react-i18next"; import { Toggle } from "@/components/buttons/Toggle"; @@ -13,6 +14,7 @@ import { qualityToString, } from "@/stores/player/utils/qualities"; import { useQualityStore } from "@/stores/quality"; +import { canPlayHlsNatively } from "@/utils/detectFeatures"; const alwaysVisibleQualities: Record = { unknown: false, @@ -22,8 +24,21 @@ const alwaysVisibleQualities: Record = { "1080": true, }; +function useIsIosHls() { + const sourceType = usePlayerStore((s) => s.source?.type); + const result = useMemo(() => { + const videoEl = document.createElement("video"); + if (sourceType !== "hls") return false; + if (Hls.isSupported()) return false; + if (!canPlayHlsNatively(videoEl)) return false; + return true; + }, [sourceType]); + return result; +} + export function QualityView({ id }: { id: string }) { const router = useOverlayRouter(id); + const isIosHls = useIsIosHls(); const availableQualities = usePlayerStore((s) => s.qualities); const currentQuality = usePlayerStore((s) => s.currentQuality); const switchQuality = usePlayerStore((s) => s.switchQuality); @@ -61,7 +76,7 @@ export function QualityView({ id }: { id: string }) { router.navigate("/")}> {t("player.menus.quality.title")} - + {visibleQualities.map((v) => ( - + router.navigate("/source")}> text diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index d9471893..3f44bf21 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -17,6 +17,7 @@ import { canFullscreen, canFullscreenAnyElement, canPictureInPicture, + canPlayHlsNatively, canWebkitFullscreen, canWebkitPictureInPicture, } from "@/utils/detectFeatures"; @@ -69,6 +70,10 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { } function setupQualityForHls() { + if (videoElement && canPlayHlsNatively(videoElement)) { + return; // nothing to change + } + if (!hls) return; if (!automaticQuality) { const qualities = hlsLevelsToQualities(hls.levels); @@ -95,8 +100,13 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { function setupSource(vid: HTMLVideoElement, src: LoadableSource) { if (src.type === "hls") { - if (!Hls.isSupported()) throw new Error("HLS not supported"); + if (canPlayHlsNatively(vid)) { + vid.src = src.url; + vid.currentTime = startAt; + return; + } + if (!Hls.isSupported()) throw new Error("HLS not supported"); if (!hls) { hls = new Hls({ maxBufferSize: 500 * 1000 * 1000, // 500 mb of buffering, should load more fragments at once @@ -178,6 +188,14 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { emit("time", videoElement?.currentTime ?? 0) ); videoElement.addEventListener("loadedmetadata", () => { + if ( + source?.type === "hls" && + videoElement && + canPlayHlsNatively(videoElement) + ) { + emit("qualities", ["unknown"]); + emit("changedquality", "unknown"); + } emit("duration", videoElement?.duration ?? 0); }); videoElement.addEventListener("progress", () => { diff --git a/src/components/player/display/displayInterface.ts b/src/components/player/display/displayInterface.ts index 3109180b..c4c33d0d 100644 --- a/src/components/player/display/displayInterface.ts +++ b/src/components/player/display/displayInterface.ts @@ -1,7 +1,7 @@ import { LoadableSource, SourceQuality } from "@/stores/player/utils/qualities"; import { Listener } from "@/utils/events"; -export type DisplayErrorType = "hls" | "htmlvideo"; +export type DisplayErrorType = "hls" | "htmlvideo" | "global"; export type DisplayError = { stackTrace?: string; message?: string; diff --git a/src/components/player/hooks/useShouldShowControls.tsx b/src/components/player/hooks/useShouldShowControls.tsx index 30eac99a..58a70c09 100644 --- a/src/components/player/hooks/useShouldShowControls.tsx +++ b/src/components/player/hooks/useShouldShowControls.tsx @@ -17,7 +17,7 @@ export function useShouldShowControls() { // when using touch, pause screens can be dismissed by tapping const showTargetsWithoutPause = - isHovering || isHoveringControls || hasOpenOverlay; + isHovering || (isHoveringControls && !isUsingTouch) || hasOpenOverlay; const showTargetsIncludingPause = showTargetsWithoutPause || isPaused; const showTargets = isUsingTouch ? showTargetsWithoutPause diff --git a/src/components/player/hooks/useSlashFocus.ts b/src/components/player/hooks/useSlashFocus.ts new file mode 100644 index 00000000..2c44e33f --- /dev/null +++ b/src/components/player/hooks/useSlashFocus.ts @@ -0,0 +1,22 @@ +import { useEffect } from "react"; + +export function useSlashFocus(ref: React.RefObject) { + useEffect(() => { + const listener = (e: KeyboardEvent) => { + if (e.key === "/") { + if ( + document.activeElement && + document.activeElement.tagName.toLowerCase() === "input" + ) + return; + e.preventDefault(); + ref.current?.focus(); + } + }; + + window.addEventListener("keydown", listener); + return () => { + window.removeEventListener("keydown", listener); + }; + }, [ref]); +} diff --git a/src/components/player/internals/ContextMenu/Cards.tsx b/src/components/player/internals/ContextMenu/Cards.tsx index 12225489..c650967e 100644 --- a/src/components/player/internals/ContextMenu/Cards.tsx +++ b/src/components/player/internals/ContextMenu/Cards.tsx @@ -1,7 +1,7 @@ export function Card(props: { children: React.ReactNode }) { return (
-
+
{props.children}
diff --git a/src/components/player/utils/videoTracks.ts b/src/components/player/utils/videoTracks.ts new file mode 100644 index 00000000..3a0fbdb2 --- /dev/null +++ b/src/components/player/utils/videoTracks.ts @@ -0,0 +1,19 @@ +export interface VideoTrack { + selected: boolean; + id: string; + kind: string; + label: string; + language: string; +} + +export type VideoTrackList = Array & { + selectedIndex: number; + getTrackById(id: string): VideoTrack | null; + addEventListener(type: "change", listener: (ev: Event) => any): void; +}; + +export function getVideoTracks(video: HTMLVideoElement): VideoTrackList | null { + const videoAsAny = video as any; + if (!videoAsAny.videoTracks) return null; + return videoAsAny.videoTracks; +} diff --git a/src/components/text-inputs/TextInputControl.tsx b/src/components/text-inputs/TextInputControl.tsx index 7b95f219..b555c4f8 100644 --- a/src/components/text-inputs/TextInputControl.tsx +++ b/src/components/text-inputs/TextInputControl.tsx @@ -1,3 +1,5 @@ +import { forwardRef } from "react"; + export interface TextInputControlPropsNoLabel { onChange?: (data: string) => void; onUnFocus?: () => void; @@ -13,39 +15,51 @@ export interface TextInputControlProps extends TextInputControlPropsNoLabel { label?: string; } -export function TextInputControl({ - onChange, - onUnFocus, - value, - label, - name, - autoComplete, - className, - placeholder, - onFocus, -}: TextInputControlProps) { - const input = ( - onChange && onChange(e.target.value)} - value={value} - name={name} - autoComplete={autoComplete} - onBlur={() => onUnFocus && onUnFocus()} - onFocus={() => onFocus?.()} - /> - ); - - if (label) { - return ( - +export const TextInputControl = forwardRef< + HTMLInputElement, + TextInputControlProps +>( + ( + { + onChange, + onUnFocus, + value, + label, + name, + autoComplete, + className, + placeholder, + onFocus, + }, + ref + ) => { + const input = ( + onChange && onChange(e.target.value)} + value={value} + name={name} + autoComplete={autoComplete} + onBlur={() => onUnFocus && onUnFocus()} + onFocus={() => onFocus?.()} + onKeyDown={(e) => + e.key === "Enter" ? (e.target as HTMLInputElement).blur() : null + } + /> ); - } - return input; -} + if (label) { + return ( + + ); + } + + return input; + } +); diff --git a/src/components/utils/Lightbar.css b/src/components/utils/Lightbar.css index 7fe7572b..06f7ffe8 100644 --- a/src/components/utils/Lightbar.css +++ b/src/components/utils/Lightbar.css @@ -1,6 +1,5 @@ .lightbar, .lightbar-visual { position: absolute; - top: 0; width: 500vw; height: 800px; pointer-events: none; diff --git a/src/components/utils/Lightbar.tsx b/src/components/utils/Lightbar.tsx index 0d30b5ad..59d4cac8 100644 --- a/src/components/utils/Lightbar.tsx +++ b/src/components/utils/Lightbar.tsx @@ -1,4 +1,3 @@ -import classNames from "classnames"; import { useEffect, useRef } from "react"; import "./Lightbar.css"; @@ -162,15 +161,14 @@ function ParticlesCanvas() { export function Lightbar(props: { className?: string }) { return ( -
-
- -
+
+
+
+
+ +
+
+
); diff --git a/src/components/utils/Text.tsx b/src/components/utils/Text.tsx index 27d2ecb4..7392b820 100644 --- a/src/components/utils/Text.tsx +++ b/src/components/utils/Text.tsx @@ -24,7 +24,7 @@ export function Heading2(props: TextProps) { return (

void, @@ -8,10 +12,10 @@ export function useSearchQuery(): [ ] { const history = useHistory(); const params = useParams<{ query: string }>(); - const [search, setSearch] = useState(params.query ?? ""); + const [search, setSearch] = useState(decode(params.query)); useEffect(() => { - setSearch(params.query ?? ""); + setSearch(decode(params.query)); }, [params.query]); const updateParams = (inp: string, commitToUrl = false) => { diff --git a/src/pages/About.tsx b/src/pages/About.tsx index c57ed706..fc916c4c 100644 --- a/src/pages/About.tsx +++ b/src/pages/About.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { ThinContainer } from "@/components/layout/ThinContainer"; import { Ol } from "@/components/utils/Ol"; import { Heading1, Heading2, Paragraph } from "@/components/utils/Text"; +import { PageTitle } from "@/pages/parts/util/PageTitle"; import { SubPageLayout } from "./layouts/SubPageLayout"; @@ -19,6 +20,7 @@ export function AboutPage() { const { t } = useTranslation(); return ( + {t("faq.title")}
    + {t("screens.dmca.title")} {t("screens.dmca.text")} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index fbf23c50..4c60a669 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -2,12 +2,14 @@ import { useHistory } from "react-router-dom"; import { SubPageLayout } from "@/pages/layouts/SubPageLayout"; import { LoginFormPart } from "@/pages/parts/auth/LoginFormPart"; +import { PageTitle } from "@/pages/parts/util/PageTitle"; export function LoginPage() { const history = useHistory(); return ( + { history.push("/"); diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index ce528fb5..ca85a2bf 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -11,6 +11,7 @@ import { import { PassphraseGeneratePart } from "@/pages/parts/auth/PassphraseGeneratePart"; import { TrustBackendPart } from "@/pages/parts/auth/TrustBackendPart"; import { VerifyPassphrase } from "@/pages/parts/auth/VerifyPassphrasePart"; +import { PageTitle } from "@/pages/parts/util/PageTitle"; function CaptchaProvider(props: { siteKey: string | null; @@ -34,6 +35,7 @@ export function RegisterPage() { return ( + {step === 0 ? ( { diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 30ed4e5e..baea3c96 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -15,6 +15,7 @@ import { Button } from "@/components/buttons/Button"; import { WideContainer } from "@/components/layout/WideContainer"; import { UserIcons } from "@/components/UserIcon"; import { Heading1 } from "@/components/utils/Text"; +import { Transition } from "@/components/utils/Transition"; import { useAuth } from "@/hooks/auth/useAuth"; import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useIsMobile } from "@/hooks/useIsMobile"; @@ -27,6 +28,7 @@ import { DeviceListPart } from "@/pages/parts/settings/DeviceListPart"; import { RegisterCalloutPart } from "@/pages/parts/settings/RegisterCalloutPart"; import { SidebarPart } from "@/pages/parts/settings/SidebarPart"; import { ThemePart } from "@/pages/parts/settings/ThemePart"; +import { PageTitle } from "@/pages/parts/util/PageTitle"; import { AccountWithToken, useAuthStore } from "@/stores/auth"; import { useLanguageStore } from "@/stores/language"; import { useSubtitleStore } from "@/stores/subtitles"; @@ -190,6 +192,7 @@ export function SettingsPage() { ]); return ( +
    @@ -241,10 +244,10 @@ export function SettingsPage() { />
    -

    {t("settings.unsaved")}

    @@ -263,7 +266,7 @@ export function SettingsPage() { {t("settings.save")}
    -
    +
    ); } diff --git a/src/pages/parts/errors/ErrorPart.tsx b/src/pages/parts/errors/ErrorPart.tsx index 90109232..e61ec744 100644 --- a/src/pages/parts/errors/ErrorPart.tsx +++ b/src/pages/parts/errors/ErrorPart.tsx @@ -3,25 +3,35 @@ import { useTranslation } from "react-i18next"; import { ButtonPlain } from "@/components/buttons/Button"; import { Icons } from "@/components/Icon"; import { IconPill } from "@/components/layout/IconPill"; +import { DisplayError } from "@/components/player/display/displayInterface"; import { Title } from "@/components/text/Title"; import { Paragraph } from "@/components/utils/Text"; import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout"; +import { ErrorCard } from "@/pages/parts/errors/ErrorCard"; export function ErrorPart(props: { error: any; errorInfo: any }) { - const data = JSON.stringify({ - error: props.error, - errorInfo: props.errorInfo, - }); const { t } = useTranslation(); + const maxLineCount = 5; + const errorLines = (props.errorInfo.componentStack || "") + .split("\n") + .slice(0, maxLineCount); + + const error: DisplayError = { + errorName: "What does this do", + type: "global", + message: errorLines.join("\n"), + }; + return ( -
    +
    - + {t("errors.badge")} {t("errors.title")} - {data} + {props.error.toString()} + (null); + useSlashFocus(inputRef); + return (
    @@ -48,6 +52,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) { onFixedToggle={stickStateChanged} > { @@ -83,15 +83,15 @@ export function SidebarPart() { if (!el) return { distance: Infinity, link: link.id }; const rect = el.getBoundingClientRect(); - const distanceTop = Math.abs(middle - rect.top); - const distanceBottom = Math.abs(middle - rect.bottom); + const distanceTop = Math.abs(centerTarget - rect.top); + const distanceBottom = Math.abs(centerTarget - rect.bottom); const distance = Math.min(distanceBottom, distanceTop); return { distance, link: link.id }; }) .sort((a, b) => a.distance - b.distance); - // shortest distance to middle of screen is the active link + // shortest distance to the part of the screen we want is the active link setActiveLink(viewList[0]?.link ?? ""); } document.addEventListener("scroll", recheck); @@ -151,10 +151,10 @@ export function SidebarPart() { {/* Backend URL */}
    -

    - {t("settings.sidebar.info.backendUrl")} +

    +

    {t("settings.sidebar.info.backendUrl")}

    -

    +

    {backendUrl.replace(/https?:\/\//, "")}

    @@ -193,7 +193,7 @@ export function SidebarPart() { /> ) : null} {backendMeta.loading ? ( -
    + ) : ( backendMeta?.value?.version || t("settings.sidebar.info.unknownVersion") diff --git a/src/pages/parts/util/PageTitle.tsx b/src/pages/parts/util/PageTitle.tsx new file mode 100644 index 00000000..c8225b45 --- /dev/null +++ b/src/pages/parts/util/PageTitle.tsx @@ -0,0 +1,20 @@ +import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; + +export interface PageTitleProps { + k: string; + subpage?: boolean; +} + +export function PageTitle(props: PageTitleProps) { + const { t } = useTranslation(); + + const title = t(props.k); + const subPageTitle = t("global.pages.pagetitle", { title }); + + return ( + + {props.subpage ? subPageTitle : title} + + ); +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 81eda406..03b1e69c 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -13,6 +13,7 @@ import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb"; import { useOnlineListener } from "@/hooks/usePing"; import { AboutPage } from "@/pages/About"; import { AdminPage } from "@/pages/admin/AdminPage"; +import VideoTesterView from "@/pages/developer/VideoTesterView"; import { DmcaPage } from "@/pages/Dmca"; import { NotFoundPage } from "@/pages/errors/NotFoundPage"; import { HomePage } from "@/pages/HomePage"; @@ -106,15 +107,12 @@ function App() { path="/dev" component={lazy(() => import("@/pages/DeveloperPage"))} /> - import("@/pages/developer/VideoTesterView"))} - /> + + + {/* developer routes that can abuse workers are disabled in production */} {process.env.NODE_ENV === "development" ? ( import("@/pages/developer/TestView"))} /> diff --git a/src/setup/i18n.ts b/src/setup/i18n.ts index 84402df1..ac86edcc 100644 --- a/src/setup/i18n.ts +++ b/src/setup/i18n.ts @@ -18,6 +18,13 @@ i18n.use(initReactI18next).init({ }); export const appLanguageOptions = langCodes.map((lang) => { + if (lang === "pirate") { + return { + code: "pirate", + name: "Pirate", + nativeName: "Pirate Tongue", + }; + } const [langObj] = ISO6391.getLanguages([lang]); if (!langObj) throw new Error(`Language with code ${lang} cannot be found in database`); diff --git a/src/utils/detectFeatures.ts b/src/utils/detectFeatures.ts index af62720d..a82a3de3 100644 --- a/src/utils/detectFeatures.ts +++ b/src/utils/detectFeatures.ts @@ -46,3 +46,7 @@ export function canPictureInPicture(): boolean { export function canWebkitPictureInPicture(): boolean { return "webkitSupportsPresentationMode" in document.createElement("video"); } + +export function canPlayHlsNatively(video: HTMLVideoElement): boolean { + return !!video.canPlayType("application/vnd.apple.mpegurl"); +}