From c2b52d3db866d9e4ec894910a8cd7ab459a3d82d Mon Sep 17 00:00:00 2001 From: frost768 Date: Thu, 6 Apr 2023 01:46:27 +0300 Subject: [PATCH 01/23] Add language selection --- src/setup/i18n.ts | 18 +- src/setup/iso6391.ts | 1326 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1337 insertions(+), 7 deletions(-) create mode 100644 src/setup/iso6391.ts diff --git a/src/setup/i18n.ts b/src/setup/i18n.ts index d243c0f5..3b18e3f5 100644 --- a/src/setup/i18n.ts +++ b/src/setup/i18n.ts @@ -4,7 +4,13 @@ import LanguageDetector from "i18next-browser-languagedetector"; // Languages import en from "./locales/en/translation.json"; +import { captionLanguages } from "./iso6391"; +const locales = { + en: { + translation: en, + }, +}; i18n // detect user language // learn more: https://github.com/i18next/i18next-browser-languageDetector @@ -15,16 +21,14 @@ i18n // for all options read: https://www.i18next.com/overview/configuration-options .init({ fallbackLng: "en", - - resources: { - en: { - translation: en, - }, - }, - + resources: locales, interpolation: { escapeValue: false, // not needed for react as it escapes by default }, }); +export const appLanguageOptions = captionLanguages.filter((x) => { + return Object.keys(locales).includes(x.id); +}); + export default i18n; diff --git a/src/setup/iso6391.ts b/src/setup/iso6391.ts new file mode 100644 index 00000000..3677560a --- /dev/null +++ b/src/setup/iso6391.ts @@ -0,0 +1,1326 @@ +export type LangCode = + | "none" + | "aa" + | "ab" + | "ae" + | "af" + | "ak" + | "am" + | "an" + | "ar" + | "as" + | "av" + | "ay" + | "az" + | "ba" + | "be" + | "bg" + | "bh" + | "bi" + | "bm" + | "bn" + | "bo" + | "br" + | "bs" + | "ca" + | "ce" + | "ch" + | "co" + | "cr" + | "cs" + | "cu" + | "cv" + | "cy" + | "da" + | "de" + | "dv" + | "dz" + | "ee" + | "el" + | "en" + | "eo" + | "es" + | "et" + | "eu" + | "fa" + | "ff" + | "fi" + | "fj" + | "fo" + | "fr" + | "fy" + | "ga" + | "gd" + | "gl" + | "gn" + | "gu" + | "gv" + | "ha" + | "he" + | "hi" + | "ho" + | "hr" + | "ht" + | "hu" + | "hy" + | "hz" + | "ia" + | "id" + | "ie" + | "ig" + | "ii" + | "ik" + | "io" + | "is" + | "it" + | "iu" + | "ja" + | "jv" + | "ka" + | "kg" + | "ki" + | "kj" + | "kk" + | "kl" + | "km" + | "kn" + | "ko" + | "kr" + | "ks" + | "ku" + | "kv" + | "kw" + | "ky" + | "la" + | "lb" + | "lg" + | "li" + | "ln" + | "lo" + | "lt" + | "lu" + | "lv" + | "mg" + | "mh" + | "mi" + | "mk" + | "ml" + | "mn" + | "mr" + | "ms" + | "mt" + | "my" + | "na" + | "nb" + | "nd" + | "ne" + | "ng" + | "nl" + | "nn" + | "no" + | "nr" + | "nv" + | "ny" + | "oc" + | "oj" + | "om" + | "or" + | "os" + | "pa" + | "pi" + | "pl" + | "ps" + | "pt" + | "qu" + | "rm" + | "rn" + | "ro" + | "ru" + | "rw" + | "sa" + | "sc" + | "sd" + | "se" + | "sg" + | "si" + | "sk" + | "sl" + | "sm" + | "sn" + | "so" + | "sq" + | "sr" + | "ss" + | "st" + | "su" + | "sv" + | "sw" + | "ta" + | "te" + | "tg" + | "th" + | "ti" + | "tk" + | "tl" + | "tn" + | "to" + | "tr" + | "ts" + | "tt" + | "tw" + | "ty" + | "ug" + | "uk" + | "ur" + | "uz" + | "ve" + | "vi" + | "vo" + | "wa" + | "wo" + | "xh" + | "yi" + | "yo" + | "za" + | "zh" + | "zu"; +type CaptionLanguageOptions = { + id: LangCode; + name: string; + englishName: string; + nativeName: string; +}; +// https://github.com/emvi/iso-639-1/blob/master/list.go +// MIT License +// +// Copyright (c) 2019 Emvi +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +export const captionLanguages: CaptionLanguageOptions[] = [ + { + id: "none", + englishName: "None", + name: "None", + nativeName: "None", + }, + { + id: "aa", + englishName: "Afar", + name: "Afar - Afaraf", + nativeName: "Afaraf", + }, + { + id: "ab", + englishName: "Abkhaz", + name: "Abkhaz - Аҧсуа бызшәа", + nativeName: "Аҧсуа бызшәа", + }, + { + id: "ae", + englishName: "Avestan", + name: "Avestan - Avesta", + nativeName: "Avesta", + }, + { + id: "af", + englishName: "Afrikaans", + name: "Afrikaans - Afrikaans", + nativeName: "Afrikaans", + }, + { + id: "ak", + englishName: "Akan", + name: "Akan - Akan", + nativeName: "Akan", + }, + { + id: "am", + englishName: "Amharic", + name: "Amharic - አማርኛ", + nativeName: "አማርኛ", + }, + { + id: "an", + englishName: "Aragonese", + name: "Aragonese - Aragonés", + nativeName: "Aragonés", + }, + { + id: "ar", + englishName: "Arabic", + name: "Arabic - اللغة العربية", + nativeName: "اللغة العربية", + }, + { + id: "as", + englishName: "Assamese", + name: "Assamese - অসমীয়া", + nativeName: "অসমীয়া", + }, + { + id: "av", + englishName: "Avaric", + name: "Avaric - Авар мацӀ", + nativeName: "Авар мацӀ", + }, + { + id: "ay", + englishName: "Aymara", + name: "Aymara - Aymar aru", + nativeName: "Aymar aru", + }, + { + id: "az", + englishName: "Azerbaijani", + name: "Azerbaijani - Azərbaycan dili", + nativeName: "Azərbaycan dili", + }, + { + id: "ba", + englishName: "Bashkir", + name: "Bashkir - Башҡорт теле", + nativeName: "Башҡорт теле", + }, + { + id: "be", + englishName: "Belarusian", + name: "Belarusian - Беларуская мова", + nativeName: "Беларуская мова", + }, + { + id: "bg", + englishName: "Bulgarian", + name: "Bulgarian - Български език", + nativeName: "Български език", + }, + { + id: "bh", + englishName: "Bihari", + name: "Bihari - भोजपुरी", + nativeName: "भोजपुरी", + }, + { + id: "bi", + englishName: "Bislama", + name: "Bislama - Bislama", + nativeName: "Bislama", + }, + { + id: "bm", + englishName: "Bambara", + name: "Bambara - Bamanankan", + nativeName: "Bamanankan", + }, + { + id: "bn", + englishName: "Bengali", + name: "Bengali - বাংলা", + nativeName: "বাংলা", + }, + { + id: "bo", + englishName: "Tibetan Standard", + name: "Tibetan Standard - བོད་ཡིག", + nativeName: "བོད་ཡིག", + }, + { + id: "br", + englishName: "Breton", + name: "Breton - Brezhoneg", + nativeName: "Brezhoneg", + }, + { + id: "bs", + englishName: "Bosnian", + name: "Bosnian - Bosanski jezik", + nativeName: "Bosanski jezik", + }, + { + id: "ca", + englishName: "Catalan", + name: "Catalan - Català", + nativeName: "Català", + }, + { + id: "ce", + englishName: "Chechen", + name: "Chechen - Нохчийн мотт", + nativeName: "Нохчийн мотт", + }, + { + id: "ch", + englishName: "Chamorro", + name: "Chamorro - Chamoru", + nativeName: "Chamoru", + }, + { + id: "co", + englishName: "Corsican", + name: "Corsican - Corsu", + nativeName: "Corsu", + }, + { + id: "cr", + englishName: "Cree", + name: "Cree - ᓀᐦᐃᔭᐍᐏᐣ", + nativeName: "ᓀᐦᐃᔭᐍᐏᐣ", + }, + { + id: "cs", + englishName: "Czech", + name: "Czech - Čeština", + nativeName: "Čeština", + }, + { + id: "cu", + englishName: "Old Church Slavonic", + name: "Old Church Slavonic - Ѩзыкъ словѣньскъ", + nativeName: "Ѩзыкъ словѣньскъ", + }, + { + id: "cv", + englishName: "Chuvash", + name: "Chuvash - Чӑваш чӗлхи", + nativeName: "Чӑваш чӗлхи", + }, + { + id: "cy", + englishName: "Welsh", + name: "Welsh - Cymraeg", + nativeName: "Cymraeg", + }, + { + id: "da", + englishName: "Danish", + name: "Danish - Dansk", + nativeName: "Dansk", + }, + { + id: "de", + englishName: "German", + name: "German - Deutsch", + nativeName: "Deutsch", + }, + { + id: "dv", + englishName: "Divehi", + name: "Divehi - Dhivehi", + nativeName: "Dhivehi", + }, + { + id: "dz", + englishName: "Dzongkha", + name: "Dzongkha - རྫོང་ཁ", + nativeName: "རྫོང་ཁ", + }, + { + id: "ee", + englishName: "Ewe", + name: "Ewe - Eʋegbe", + nativeName: "Eʋegbe", + }, + { + id: "el", + englishName: "Greek", + name: "Greek - Ελληνικά", + nativeName: "Ελληνικά", + }, + { + id: "en", + englishName: "English", + name: "English - English", + nativeName: "English", + }, + { + id: "eo", + englishName: "Esperanto", + name: "Esperanto - Esperanto", + nativeName: "Esperanto", + }, + { + id: "es", + englishName: "Spanish", + name: "Spanish - Español", + nativeName: "Español", + }, + { + id: "et", + englishName: "Estonian", + name: "Estonian - Eesti", + nativeName: "Eesti", + }, + { + id: "eu", + englishName: "Basque", + name: "Basque - Euskara", + nativeName: "Euskara", + }, + { + id: "fa", + englishName: "Persian", + name: "Persian - فارسی", + nativeName: "فارسی", + }, + { + id: "ff", + englishName: "Fula", + name: "Fula - Fulfulde", + nativeName: "Fulfulde", + }, + { + id: "fi", + englishName: "Finnish", + name: "Finnish - Suomi", + nativeName: "Suomi", + }, + { + id: "fj", + englishName: "Fijian", + name: "Fijian - Vakaviti", + nativeName: "Vakaviti", + }, + { + id: "fo", + englishName: "Faroese", + name: "Faroese - Føroyskt", + nativeName: "Føroyskt", + }, + { + id: "fr", + englishName: "French", + name: "French - Français", + nativeName: "Français", + }, + { + id: "fy", + englishName: "Western Frisian", + name: "Western Frisian - Frysk", + nativeName: "Frysk", + }, + { + id: "ga", + englishName: "Irish", + name: "Irish - Gaeilge", + nativeName: "Gaeilge", + }, + { + id: "gd", + englishName: "Scottish Gaelic", + name: "Scottish Gaelic - Gàidhlig", + nativeName: "Gàidhlig", + }, + { + id: "gl", + englishName: "Galician", + name: "Galician - Galego", + nativeName: "Galego", + }, + { + id: "gn", + englishName: "Guaraní", + name: "Guaraní - Avañeẽ", + nativeName: "Avañeẽ", + }, + { + id: "gu", + englishName: "Gujarati", + name: "Gujarati - ગુજરાતી", + nativeName: "ગુજરાતી", + }, + { + id: "gv", + englishName: "Manx", + name: "Manx - Gaelg", + nativeName: "Gaelg", + }, + { + id: "ha", + englishName: "Hausa", + name: "Hausa - هَوُسَ", + nativeName: "هَوُسَ", + }, + { + id: "he", + englishName: "Hebrew", + name: "Hebrew - עברית", + nativeName: "עברית", + }, + { + id: "hi", + englishName: "Hindi", + name: "Hindi - हिन्दी", + nativeName: "हिन्दी", + }, + { + id: "ho", + englishName: "Hiri Motu", + name: "Hiri Motu - Hiri Motu", + nativeName: "Hiri Motu", + }, + { + id: "hr", + englishName: "Croatian", + name: "Croatian - Hrvatski jezik", + nativeName: "Hrvatski jezik", + }, + { + id: "ht", + englishName: "Haitian", + name: "Haitian - Kreyòl ayisyen", + nativeName: "Kreyòl ayisyen", + }, + { + id: "hu", + englishName: "Hungarian", + name: "Hungarian - Magyar", + nativeName: "Magyar", + }, + { + id: "hy", + englishName: "Armenian", + name: "Armenian - Հայերեն", + nativeName: "Հայերեն", + }, + { + id: "hz", + englishName: "Herero", + name: "Herero - Otjiherero", + nativeName: "Otjiherero", + }, + { + id: "ia", + englishName: "Interlingua", + name: "Interlingua - Interlingua", + nativeName: "Interlingua", + }, + { + id: "id", + englishName: "Indonesian", + name: "Indonesian - Indonesian", + nativeName: "Indonesian", + }, + { + id: "ie", + englishName: "Interlingue", + name: "Interlingue - Interlingue", + nativeName: "Interlingue", + }, + { + id: "ig", + englishName: "Igbo", + name: "Igbo - Asụsụ Igbo", + nativeName: "Asụsụ Igbo", + }, + { + id: "ii", + englishName: "Nuosu", + name: "Nuosu - ꆈꌠ꒿ Nuosuhxop", + nativeName: "ꆈꌠ꒿ Nuosuhxop", + }, + { + id: "ik", + englishName: "Inupiaq", + name: "Inupiaq - Iñupiaq", + nativeName: "Iñupiaq", + }, + { + id: "io", + englishName: "Ido", + name: "Ido - Ido", + nativeName: "Ido", + }, + { + id: "is", + englishName: "Icelandic", + name: "Icelandic - Íslenska", + nativeName: "Íslenska", + }, + { + id: "it", + englishName: "Italian", + name: "Italian - Italiano", + nativeName: "Italiano", + }, + { + id: "iu", + englishName: "Inuktitut", + name: "Inuktitut - ᐃᓄᒃᑎᑐᑦ", + nativeName: "ᐃᓄᒃᑎᑐᑦ", + }, + { + id: "ja", + englishName: "Japanese", + name: "Japanese - 日本語", + nativeName: "日本語", + }, + { + id: "jv", + englishName: "Javanese", + name: "Javanese - Basa Jawa", + nativeName: "Basa Jawa", + }, + { + id: "ka", + englishName: "Georgian", + name: "Georgian - Ქართული", + nativeName: "Ქართული", + }, + { + id: "kg", + englishName: "Kongo", + name: "Kongo - Kikongo", + nativeName: "Kikongo", + }, + { + id: "ki", + englishName: "Kikuyu", + name: "Kikuyu - Gĩkũyũ", + nativeName: "Gĩkũyũ", + }, + { + id: "kj", + englishName: "Kwanyama", + name: "Kwanyama - Kuanyama", + nativeName: "Kuanyama", + }, + { + id: "kk", + englishName: "Kazakh", + name: "Kazakh - Қазақ тілі", + nativeName: "Қазақ тілі", + }, + { + id: "kl", + englishName: "Kalaallisut", + name: "Kalaallisut - Kalaallisut", + nativeName: "Kalaallisut", + }, + { + id: "km", + englishName: "Khmer", + name: "Khmer - ខេមរភាសា", + nativeName: "ខេមរភាសា", + }, + { + id: "kn", + englishName: "Kannada", + name: "Kannada - ಕನ್ನಡ", + nativeName: "ಕನ್ನಡ", + }, + { + id: "ko", + englishName: "Korean", + name: "Korean - 한국어", + nativeName: "한국어", + }, + { + id: "kr", + englishName: "Kanuri", + name: "Kanuri - Kanuri", + nativeName: "Kanuri", + }, + { + id: "ks", + englishName: "Kashmiri", + name: "Kashmiri - कश्मीरी", + nativeName: "कश्मीरी", + }, + { + id: "ku", + englishName: "Kurdish", + name: "Kurdish - Kurdî", + nativeName: "Kurdî", + }, + { + id: "kv", + englishName: "Komi", + name: "Komi - Коми кыв", + nativeName: "Коми кыв", + }, + { + id: "kw", + englishName: "Cornish", + name: "Cornish - Kernewek", + nativeName: "Kernewek", + }, + { + id: "ky", + englishName: "Kyrgyz", + name: "Kyrgyz - Кыргызча", + nativeName: "Кыргызча", + }, + { + id: "la", + englishName: "Latin", + name: "Latin - Latine", + nativeName: "Latine", + }, + { + id: "lb", + englishName: "Luxembourgish", + name: "Luxembourgish - Lëtzebuergesch", + nativeName: "Lëtzebuergesch", + }, + { + id: "lg", + englishName: "Ganda", + name: "Ganda - Luganda", + nativeName: "Luganda", + }, + { + id: "li", + englishName: "Limburgish", + name: "Limburgish - Limburgs", + nativeName: "Limburgs", + }, + { + id: "ln", + englishName: "Lingala", + name: "Lingala - Lingála", + nativeName: "Lingála", + }, + { + id: "lo", + englishName: "Lao", + name: "Lao - ພາສາ", + nativeName: "ພາສາ", + }, + { + id: "lt", + englishName: "Lithuanian", + name: "Lithuanian - Lietuvių kalba", + nativeName: "Lietuvių kalba", + }, + { + id: "lu", + englishName: "Luba-Katanga", + name: "Luba-Katanga - Tshiluba", + nativeName: "Tshiluba", + }, + { + id: "lv", + englishName: "Latvian", + name: "Latvian - Latviešu valoda", + nativeName: "Latviešu valoda", + }, + { + id: "mg", + englishName: "Malagasy", + name: "Malagasy - Fiteny malagasy", + nativeName: "Fiteny malagasy", + }, + { + id: "mh", + englishName: "Marshallese", + name: "Marshallese - Kajin M̧ajeļ", + nativeName: "Kajin M̧ajeļ", + }, + { + id: "mi", + englishName: "Māori", + name: "Māori - Te reo Māori", + nativeName: "Te reo Māori", + }, + { + id: "mk", + englishName: "Macedonian", + name: "Macedonian - Македонски јазик", + nativeName: "Македонски јазик", + }, + { + id: "ml", + englishName: "Malayalam", + name: "Malayalam - മലയാളം", + nativeName: "മലയാളം", + }, + { + id: "mn", + englishName: "Mongolian", + name: "Mongolian - Монгол хэл", + nativeName: "Монгол хэл", + }, + { + id: "mr", + englishName: "Marathi", + name: "Marathi - मराठी", + nativeName: "मराठी", + }, + { + id: "ms", + englishName: "Malay", + name: "Malay - هاس ملايو‎", + nativeName: "هاس ملايو‎", + }, + { + id: "mt", + englishName: "Maltese", + name: "Maltese - Malti", + nativeName: "Malti", + }, + { + id: "my", + englishName: "Burmese", + name: "Burmese - ဗမာစာ", + nativeName: "ဗမာစာ", + }, + { + id: "na", + englishName: "Nauru", + name: "Nauru - Ekakairũ Naoero", + nativeName: "Ekakairũ Naoero", + }, + { + id: "nb", + englishName: "Norwegian Bokmål", + name: "Norwegian Bokmål - Norsk bokmål", + nativeName: "Norsk bokmål", + }, + { + id: "nd", + englishName: "Northern Ndebele", + name: "Northern Ndebele - IsiNdebele", + nativeName: "IsiNdebele", + }, + { + id: "ne", + englishName: "Nepali", + name: "Nepali - नेपाली", + nativeName: "नेपाली", + }, + { + id: "ng", + englishName: "Ndonga", + name: "Ndonga - Owambo", + nativeName: "Owambo", + }, + { + id: "nl", + englishName: "Dutch", + name: "Dutch - Nederlands", + nativeName: "Nederlands", + }, + { + id: "nn", + englishName: "Norwegian Nynorsk", + name: "Norwegian Nynorsk - Norsk nynorsk", + nativeName: "Norsk nynorsk", + }, + { + id: "no", + englishName: "Norwegian", + name: "Norwegian - Norsk", + nativeName: "Norsk", + }, + { + id: "nr", + englishName: "Southern Ndebele", + name: "Southern Ndebele - IsiNdebele", + nativeName: "IsiNdebele", + }, + { + id: "nv", + englishName: "Navajo", + name: "Navajo - Diné bizaad", + nativeName: "Diné bizaad", + }, + { + id: "ny", + englishName: "Chichewa", + name: "Chichewa - ChiCheŵa", + nativeName: "ChiCheŵa", + }, + { + id: "oc", + englishName: "Occitan", + name: "Occitan - Occitan", + nativeName: "Occitan", + }, + { + id: "oj", + englishName: "Ojibwe", + name: "Ojibwe - ᐊᓂᔑᓈᐯᒧᐎᓐ", + nativeName: "ᐊᓂᔑᓈᐯᒧᐎᓐ", + }, + { + id: "om", + englishName: "Oromo", + name: "Oromo - Afaan Oromoo", + nativeName: "Afaan Oromoo", + }, + { + id: "or", + englishName: "Oriya", + name: "Oriya - ଓଡ଼ିଆ", + nativeName: "ଓଡ଼ିଆ", + }, + { + id: "os", + englishName: "Ossetian", + name: "Ossetian - Ирон æвзаг", + nativeName: "Ирон æвзаг", + }, + { + id: "pa", + englishName: "Panjabi", + name: "Panjabi - ਪੰਜਾਬੀ", + nativeName: "ਪੰਜਾਬੀ", + }, + { + id: "pi", + englishName: "Pāli", + name: "Pāli - पाऴि", + nativeName: "पाऴि", + }, + { + id: "pl", + englishName: "Polish", + name: "Polish - Język polski", + nativeName: "Język polski", + }, + { + id: "ps", + englishName: "Pashto", + name: "Pashto - پښتو", + nativeName: "پښتو", + }, + { + id: "pt", + englishName: "Portuguese", + name: "Portuguese - Português", + nativeName: "Português", + }, + { + id: "qu", + englishName: "Quechua", + name: "Quechua - Runa Simi", + nativeName: "Runa Simi", + }, + { + id: "rm", + englishName: "Romansh", + name: "Romansh - Rumantsch grischun", + nativeName: "Rumantsch grischun", + }, + { + id: "rn", + englishName: "Kirundi", + name: "Kirundi - Ikirundi", + nativeName: "Ikirundi", + }, + { + id: "ro", + englishName: "Romanian", + name: "Romanian - Română", + nativeName: "Română", + }, + { + id: "ru", + englishName: "Russian", + name: "Russian - Русский", + nativeName: "Русский", + }, + { + id: "rw", + englishName: "Kinyarwanda", + name: "Kinyarwanda - Ikinyarwanda", + nativeName: "Ikinyarwanda", + }, + { + id: "sa", + englishName: "Sanskrit", + name: "Sanskrit - संस्कृतम्", + nativeName: "संस्कृतम्", + }, + { + id: "sc", + englishName: "Sardinian", + name: "Sardinian - Sardu", + nativeName: "Sardu", + }, + { + id: "sd", + englishName: "Sindhi", + name: "Sindhi - सिन्धी", + nativeName: "सिन्धी", + }, + { + id: "se", + englishName: "Northern Sami", + name: "Northern Sami - Davvisámegiella", + nativeName: "Davvisámegiella", + }, + { + id: "sg", + englishName: "Sango", + name: "Sango - Yângâ tî sängö", + nativeName: "Yângâ tî sängö", + }, + { + id: "si", + englishName: "Sinhala", + name: "Sinhala - සිංහල", + nativeName: "සිංහල", + }, + { + id: "sk", + englishName: "Slovak", + name: "Slovak - Slovenčina", + nativeName: "Slovenčina", + }, + { + id: "sl", + englishName: "Slovene", + name: "Slovene - Slovenski jezik", + nativeName: "Slovenski jezik", + }, + { + id: "sm", + englishName: "Samoan", + name: "Samoan - Gagana faa Samoa", + nativeName: "Gagana faa Samoa", + }, + { + id: "sn", + englishName: "Shona", + name: "Shona - ChiShona", + nativeName: "ChiShona", + }, + { + id: "so", + englishName: "Somali", + name: "Somali - Soomaaliga", + nativeName: "Soomaaliga", + }, + { + id: "sq", + englishName: "Albanian", + name: "Albanian - Shqip", + nativeName: "Shqip", + }, + { + id: "sr", + englishName: "Serbian", + name: "Serbian - Српски језик", + nativeName: "Српски језик", + }, + { + id: "ss", + englishName: "Swati", + name: "Swati - SiSwati", + nativeName: "SiSwati", + }, + { + id: "st", + englishName: "Southern Sotho", + name: "Southern Sotho - Sesotho", + nativeName: "Sesotho", + }, + { + id: "su", + englishName: "Sundanese", + name: "Sundanese - Basa Sunda", + nativeName: "Basa Sunda", + }, + { + id: "sv", + englishName: "Swedish", + name: "Swedish - Svenska", + nativeName: "Svenska", + }, + { + id: "sw", + englishName: "Swahili", + name: "Swahili - Kiswahili", + nativeName: "Kiswahili", + }, + { + id: "ta", + englishName: "Tamil", + name: "Tamil - தமிழ்", + nativeName: "தமிழ்", + }, + { + id: "te", + englishName: "Telugu", + name: "Telugu - తెలుగు", + nativeName: "తెలుగు", + }, + { + id: "tg", + englishName: "Tajik", + name: "Tajik - Тоҷикӣ", + nativeName: "Тоҷикӣ", + }, + { + id: "th", + englishName: "Thai", + name: "Thai - ไทย", + nativeName: "ไทย", + }, + { + id: "ti", + englishName: "Tigrinya", + name: "Tigrinya - ትግርኛ", + nativeName: "ትግርኛ", + }, + { + id: "tk", + englishName: "Turkmen", + name: "Turkmen - Türkmen", + nativeName: "Türkmen", + }, + { + id: "tl", + englishName: "Tagalog", + name: "Tagalog - Wikang Tagalog", + nativeName: "Wikang Tagalog", + }, + { + id: "tn", + englishName: "Tswana", + name: "Tswana - Setswana", + nativeName: "Setswana", + }, + { + id: "to", + englishName: "Tonga", + name: "Tonga - Faka Tonga", + nativeName: "Faka Tonga", + }, + { + id: "tr", + englishName: "Turkish", + name: "Turkish - Türkçe", + nativeName: "Türkçe", + }, + { + id: "ts", + englishName: "Tsonga", + name: "Tsonga - Xitsonga", + nativeName: "Xitsonga", + }, + { + id: "tt", + englishName: "Tatar", + name: "Tatar - Татар теле", + nativeName: "Татар теле", + }, + { + id: "tw", + englishName: "Twi", + name: "Twi - Twi", + nativeName: "Twi", + }, + { + id: "ty", + englishName: "Tahitian", + name: "Tahitian - Reo Tahiti", + nativeName: "Reo Tahiti", + }, + { + id: "ug", + englishName: "Uyghur", + name: "Uyghur - ئۇيغۇرچە‎", + nativeName: "ئۇيغۇرچە‎", + }, + { + id: "uk", + englishName: "Ukrainian", + name: "Ukrainian - Українська", + nativeName: "Українська", + }, + { + id: "ur", + englishName: "Urdu", + name: "Urdu - اردو", + nativeName: "اردو", + }, + { + id: "uz", + englishName: "Uzbek", + name: "Uzbek - Ўзбек", + nativeName: "Ўзбек", + }, + { + id: "ve", + englishName: "Venda", + name: "Venda - Tshivenḓa", + nativeName: "Tshivenḓa", + }, + { + id: "vi", + englishName: "Vietnamese", + name: "Vietnamese - Tiếng Việt", + nativeName: "Tiếng Việt", + }, + { + id: "vo", + englishName: "Volapük", + name: "Volapük - Volapük", + nativeName: "Volapük", + }, + { + id: "wa", + englishName: "Walloon", + name: "Walloon - Walon", + nativeName: "Walon", + }, + { + id: "wo", + englishName: "Wolof", + name: "Wolof - Wollof", + nativeName: "Wollof", + }, + { + id: "xh", + englishName: "Xhosa", + name: "Xhosa - IsiXhosa", + nativeName: "IsiXhosa", + }, + { + id: "yi", + englishName: "Yiddish", + name: "Yiddish - ייִדיש", + nativeName: "ייִדיש", + }, + { + id: "yo", + englishName: "Yoruba", + name: "Yoruba - Yorùbá", + nativeName: "Yorùbá", + }, + { + id: "za", + englishName: "Zhuang", + name: "Zhuang - Saɯ cueŋƅ", + nativeName: "Saɯ cueŋƅ", + }, + { + id: "zh", + englishName: "Chinese", + name: "Chinese - 中文", + nativeName: "中文", + }, + { + id: "zu", + englishName: "Zulu", + name: "Zulu - IsiZulu", + nativeName: "IsiZulu", + }, +]; From 9e961223f6edae1b66621ce16f420bb16843b800 Mon Sep 17 00:00:00 2001 From: frost768 Date: Thu, 6 Apr 2023 01:48:07 +0300 Subject: [PATCH 02/23] settings modal --- src/components/Dropdown.tsx | 2 +- src/components/layout/Modal.tsx | 9 +- src/components/layout/Navigation.tsx | 14 +- src/setup/locales/en/translation.json | 5 + src/state/settings/context.tsx | 21 +-- src/state/settings/store.ts | 28 +++- src/state/settings/types.ts | 18 ++- .../actions/CaptionRendererAction.tsx | 9 +- src/views/SettingsModal.tsx | 142 ++++++++++++++++++ 9 files changed, 228 insertions(+), 20 deletions(-) create mode 100644 src/views/SettingsModal.tsx diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx index 84ab2957..9321e1fe 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/Dropdown.tsx @@ -37,7 +37,7 @@ export function Dropdown(props: DropdownProps) { leaveFrom="opacity-100" leaveTo="opacity-0" > - + {props.options.map((opt) => ( diff --git a/src/components/layout/Modal.tsx b/src/components/layout/Modal.tsx index 7a1c3b64..8ae2f58f 100644 --- a/src/components/layout/Modal.tsx +++ b/src/components/layout/Modal.tsx @@ -35,9 +35,14 @@ export function Modal(props: Props) { ); } -export function ModalCard(props: { children?: ReactNode }) { +export function ModalCard(props: { className?: string; children?: ReactNode }) { return ( -
+
{props.children}
); diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index 4fe1864f..836c6206 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -1,9 +1,10 @@ -import { ReactNode } from "react"; +import { ReactNode, useState } from "react"; import { Link } from "react-router-dom"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { conf } from "@/setup/config"; import { useBannerSize } from "@/hooks/useBanner"; +import SettingsModal from "@/views/SettingsModal"; import { BrandPill } from "./BrandPill"; export interface NavigationProps { @@ -13,7 +14,7 @@ export interface NavigationProps { export function Navigation(props: NavigationProps) { const bannerHeight = useBannerSize(); - + const [showModal, setShowModal] = useState(false); return (
+ { + setShowModal(true); + }} + />
+ setShowModal(false)} /> ); } diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index 1838b63c..71ee7064 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -99,6 +99,11 @@ "fatalError": "The video player encounted a fatal error, please report it to the <0>Discord server or on <1>GitHub." } }, + "settings": { + "title": "Settings", + "language":"Language", + "captionLanguage": "Caption Language" + }, "v3": { "newSiteTitle": "New version now released!", "newDomain": "https://movie-web.app", diff --git a/src/state/settings/context.tsx b/src/state/settings/context.tsx index 030833cc..be7a92e0 100644 --- a/src/state/settings/context.tsx +++ b/src/state/settings/context.tsx @@ -1,14 +1,15 @@ import { useStore } from "@/utils/storage"; import { createContext, ReactNode, useContext, useMemo } from "react"; +import { LangCode } from "@/setup/iso6391"; import { SettingsStore } from "./store"; import { MWSettingsData } from "./types"; interface MWSettingsDataSetters { - setLanguage(language: string): void; + setCaptionLanguage(language: LangCode): void; setCaptionDelay(delay: number): void; setCaptionColor(color: string): void; setCaptionFontSize(size: number): void; - setCaptionBackgroundColor(backgroundColor: string): void; + setCaptionBackgroundColor(backgroundColor: number): void; } type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters; const SettingsContext = createContext(null as any); @@ -17,16 +18,15 @@ export function SettingsProvider(props: { children: ReactNode }) { return Math.max(min, Math.min(value, max)); } const [settings, setSettings] = useStore(SettingsStore); - const context: MWSettingsDataWrapper = useMemo(() => { const settingsContext: MWSettingsDataWrapper = { ...settings, - setLanguage(language) { + setCaptionLanguage(language) { setSettings((oldSettings) => { - return { - ...oldSettings, - language, - }; + const captionSettings = oldSettings.captionSettings; + captionSettings.language = language; + const newSettings = oldSettings; + return newSettings; }); }, setCaptionDelay(delay: number) { @@ -56,7 +56,10 @@ export function SettingsProvider(props: { children: ReactNode }) { setCaptionBackgroundColor(backgroundColor) { setSettings((oldSettings) => { const style = oldSettings.captionSettings.style; - style.backgroundColor = backgroundColor; + style.backgroundColor = `${style.backgroundColor.substring( + 0, + 7 + )}${backgroundColor.toString(16).padStart(2, "0")}`; const newSettings = oldSettings; return newSettings; }); diff --git a/src/state/settings/store.ts b/src/state/settings/store.ts index c854dc04..e8c629c1 100644 --- a/src/state/settings/store.ts +++ b/src/state/settings/store.ts @@ -1,11 +1,11 @@ import { createVersionedStore } from "@/utils/storage"; -import { MWSettingsData } from "./types"; +import { MWSettingsData, MWSettingsDataV1 } from "./types"; export const SettingsStore = createVersionedStore() .setKey("mw-settings") .addVersion({ version: 0, - create(): MWSettingsData { + create(): MWSettingsDataV1 { return { language: "en", captionSettings: { @@ -18,5 +18,29 @@ export const SettingsStore = createVersionedStore() }, }; }, + migrate(data: MWSettingsDataV1): MWSettingsData { + return { + captionSettings: { + language: "none", + ...data.captionSettings, + }, + }; + }, + }) + .addVersion({ + version: 1, + create(): MWSettingsData { + return { + captionSettings: { + delay: 0, + language: "none", + style: { + color: "#ffffff", + fontSize: 25, + backgroundColor: "#00000096", + }, + }, + }; + }, }) .build(); diff --git a/src/state/settings/types.ts b/src/state/settings/types.ts index b793308d..e32bd685 100644 --- a/src/state/settings/types.ts +++ b/src/state/settings/types.ts @@ -1,3 +1,5 @@ +import { LangCode } from "@/setup/iso6391"; + export interface CaptionStyleSettings { color: string; /** @@ -7,7 +9,7 @@ export interface CaptionStyleSettings { backgroundColor: string; } -export interface CaptionSettings { +export interface CaptionSettingsV1 { /** * Range is [-10, 10]s */ @@ -15,7 +17,19 @@ export interface CaptionSettings { style: CaptionStyleSettings; } +export interface CaptionSettings { + language: LangCode; + /** + * Range is [-10, 10]s + */ + delay: number; + style: CaptionStyleSettings; +} +export interface MWSettingsDataV1 { + language: LangCode; + captionSettings: CaptionSettingsV1; +} + export interface MWSettingsData { - language: string; captionSettings: CaptionSettings; } diff --git a/src/video/components/actions/CaptionRendererAction.tsx b/src/video/components/actions/CaptionRendererAction.tsx index ab7edba0..f3a65f52 100644 --- a/src/video/components/actions/CaptionRendererAction.tsx +++ b/src/video/components/actions/CaptionRendererAction.tsx @@ -8,7 +8,7 @@ import { useVideoPlayerDescriptor } from "../../state/hooks"; import { useProgress } from "../../state/logic/progress"; import { useSource } from "../../state/logic/source"; -function CaptionCue({ text }: { text?: string }) { +export function CaptionCue({ text, scale }: { text?: string; scale?: number }) { const { captionSettings } = useSettings(); const textWithNewlines = (text || "").replaceAll(/\r?\n/g, "
"); @@ -22,9 +22,14 @@ function CaptionCue({ text }: { text?: string }) { return (

void; + show: boolean; +}) { + const { + captionSettings, + setCaptionLanguage, + setCaptionBackgroundColor, + setCaptionColor, + setCaptionFontSize, + } = useSettings(); + const { t, i18n } = useTranslation(); + + const colors = ["#ffffff", "#00ffff", "#ffff00"]; + const selectedCaptionLanguage = useMemo( + () => captionLanguages.find((l) => l.id === captionSettings.language)!, + [captionSettings.language] + ); + const captionBackgroundOpacity = ( + (parseInt(captionSettings.style.backgroundColor.substring(7, 9), 16) / + 255) * + 100 + ).toFixed(0); + return ( + + +

+ {t("settings.title")} +
props.onClose()} className="hover:cursor-pointer"> + +
+
+
+
+ + l.id === i18n.language)! + } + setSelectedItem={(val) => { + i18n.changeLanguage(val.id); + }} + options={appLanguageOptions} + /> +
+
+ + { + setCaptionLanguage(val.id as LangCode); + }} + options={captionLanguages} + /> +
+
+
+
+ setCaptionFontSize(e.target.valueAsNumber)} + /> + + setCaptionBackgroundColor(e.target.valueAsNumber) + } + /> +
+ +
+ {colors.map((color) => ( +
setCaptionColor(color)} + > +
+ +
+ ))} +
+
+
+
+ {selectedCaptionLanguage.id !== "none" ? ( +
+ +
+ ) : null} +
+
+ + + ); +} From 21780576339ecf20f0b1e53bf20605bfb20f146b Mon Sep 17 00:00:00 2001 From: frost768 Date: Thu, 6 Apr 2023 01:49:33 +0300 Subject: [PATCH 03/23] auto select subtitle --- src/backend/helpers/captions.ts | 4 ++ .../controllers/SourceController.tsx | 48 +++++++++++++++++-- .../popouts/CaptionSelectionPopout.tsx | 7 +-- .../popouts/CaptionSettingsPopout.tsx | 9 +--- src/views/developer/VideoTesterView.tsx | 1 + src/views/media/MediaView.tsx | 1 + 6 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/backend/helpers/captions.ts b/src/backend/helpers/captions.ts index 83edaa84..b5e40108 100644 --- a/src/backend/helpers/captions.ts +++ b/src/backend/helpers/captions.ts @@ -4,6 +4,10 @@ import DOMPurify from "dompurify"; import { parse, detect, list } from "subsrt-ts"; import { ContentCaption } from "subsrt-ts/dist/types/handler"; +export const customCaption = "external-custom"; +export function makeCaptionId(caption: MWCaption, isLinked: boolean): string { + return isLinked ? `linked-${caption.langIso}` : `external-${caption.langIso}`; +} export const subtitleTypeList = list().map((type) => `.${type}`); export const sanitize = DOMPurify.sanitize; export async function getCaptionUrl(caption: MWCaption): Promise { diff --git a/src/video/components/controllers/SourceController.tsx b/src/video/components/controllers/SourceController.tsx index 14d5ad93..fe98a685 100644 --- a/src/video/components/controllers/SourceController.tsx +++ b/src/video/components/controllers/SourceController.tsx @@ -1,4 +1,11 @@ -import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; +import { getCaptionUrl, makeCaptionId } from "@/backend/helpers/captions"; +import { + MWCaption, + MWStreamQuality, + MWStreamType, +} from "@/backend/helpers/streams"; +import { captionLanguages } from "@/setup/iso6391"; +import { useSettings } from "@/state/settings"; import { useInitialized } from "@/video/components/hooks/useInitialized"; import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useControls } from "@/video/state/logic/controls"; @@ -10,6 +17,19 @@ interface SourceControllerProps { quality: MWStreamQuality; providerId?: string; embedId?: string; + captions: MWCaption[]; +} +async function tryFetch(captions: MWCaption[]) { + for (let i = 0; i < captions.length; i += 1) { + const caption = captions[i]; + try { + const blobUrl = await getCaptionUrl(caption); + return { caption, blobUrl }; + } catch (error) { + continue; + } + } + return null; } export function SourceController(props: SourceControllerProps) { @@ -17,13 +37,35 @@ export function SourceController(props: SourceControllerProps) { const controls = useControls(descriptor); const { initialized } = useInitialized(descriptor); const didInitialize = useRef(false); - + const { captionSettings } = useSettings(); useEffect(() => { if (didInitialize.current) return; if (!initialized) return; controls.setSource(props); + // get preferred language + const preferredLanguage = captionLanguages.find( + (v) => v.id === captionSettings.language + ); + if (!preferredLanguage) return; + const captions = props.captions.filter( + (v) => + // langIso may contain the English name or the native name of the language + v.langIso.indexOf(preferredLanguage.englishName) !== -1 || + v.langIso.indexOf(preferredLanguage.nativeName) !== -1 + ); + if (!captions) return; + // caption url can return a response other than 200 + // that's why we fetch until we get a 200 response + tryFetch(captions).then((response) => { + // none of them were successful + if (!response) return; + // set the preferred language + const id = makeCaptionId(response.caption, true); + controls.setCaption(id, response.blobUrl); + }); + didInitialize.current = true; - }, [props, controls, initialized]); + }, [props, controls, initialized, captionSettings.language]); return null; } diff --git a/src/video/components/popouts/CaptionSelectionPopout.tsx b/src/video/components/popouts/CaptionSelectionPopout.tsx index 2ae9fced..3f595757 100644 --- a/src/video/components/popouts/CaptionSelectionPopout.tsx +++ b/src/video/components/popouts/CaptionSelectionPopout.tsx @@ -1,5 +1,7 @@ import { + customCaption, getCaptionUrl, + makeCaptionId, parseSubtitles, subtitleTypeList, } from "@/backend/helpers/captions"; @@ -17,11 +19,6 @@ import { useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; -const customCaption = "external-custom"; -function makeCaptionId(caption: MWCaption, isLinked: boolean): string { - return isLinked ? `linked-${caption.langIso}` : `external-${caption.langIso}`; -} - export function CaptionSelectionPopout(props: { router: ReturnType; prefix: string; diff --git a/src/video/components/popouts/CaptionSettingsPopout.tsx b/src/video/components/popouts/CaptionSettingsPopout.tsx index 09bf6eea..d4f212a5 100644 --- a/src/video/components/popouts/CaptionSettingsPopout.tsx +++ b/src/video/components/popouts/CaptionSettingsPopout.tsx @@ -106,14 +106,7 @@ export function CaptionSettingsPopout(props: { captionSettings.style.backgroundColor.substring(7, 9), 16 )} - onChange={(e) => - setCaptionBackgroundColor( - `${captionSettings.style.backgroundColor.substring( - 0, - 7 - )}${e.target.valueAsNumber.toString(16)}` - ) - } + onChange={(e) => setCaptionBackgroundColor(e.target.valueAsNumber)} />
diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index b674fb9f..c2ee94e7 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -148,6 +148,7 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) { quality={props.stream.quality} embedId={props.stream.embedId} providerId={props.stream.providerId} + captions={props.stream.captions} /> Date: Thu, 6 Apr 2023 04:34:59 +0300 Subject: [PATCH 04/23] fix migration --- src/backend/providers/superstream/index.ts | 2 +- src/state/settings/context.tsx | 9 +++++++++ src/state/settings/store.ts | 2 ++ src/state/settings/types.ts | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts index 9ebe6262..a2469cdd 100644 --- a/src/backend/providers/superstream/index.ts +++ b/src/backend/providers/superstream/index.ts @@ -230,7 +230,7 @@ registerProvider({ const sub = subtitle; sub.subtitles = subtitle.subtitles.filter((subFile: any) => { const extension = subFile.file_path.substring( - sub.file_path.length - 3 + subFile.file_path.length - 3 ); return [MWCaptionType.SRT, MWCaptionType.VTT].includes(extension); }); diff --git a/src/state/settings/context.tsx b/src/state/settings/context.tsx index be7a92e0..8efba361 100644 --- a/src/state/settings/context.tsx +++ b/src/state/settings/context.tsx @@ -5,6 +5,7 @@ import { SettingsStore } from "./store"; import { MWSettingsData } from "./types"; interface MWSettingsDataSetters { + setLanguage(language: LangCode): void; setCaptionLanguage(language: LangCode): void; setCaptionDelay(delay: number): void; setCaptionColor(color: string): void; @@ -21,6 +22,14 @@ export function SettingsProvider(props: { children: ReactNode }) { const context: MWSettingsDataWrapper = useMemo(() => { const settingsContext: MWSettingsDataWrapper = { ...settings, + setLanguage(language) { + setSettings((oldSettings) => { + return { + ...oldSettings, + language, + }; + }); + }, setCaptionLanguage(language) { setSettings((oldSettings) => { const captionSettings = oldSettings.captionSettings; diff --git a/src/state/settings/store.ts b/src/state/settings/store.ts index e8c629c1..55ede71e 100644 --- a/src/state/settings/store.ts +++ b/src/state/settings/store.ts @@ -20,6 +20,7 @@ export const SettingsStore = createVersionedStore() }, migrate(data: MWSettingsDataV1): MWSettingsData { return { + language: data.language, captionSettings: { language: "none", ...data.captionSettings, @@ -31,6 +32,7 @@ export const SettingsStore = createVersionedStore() version: 1, create(): MWSettingsData { return { + language: "en", captionSettings: { delay: 0, language: "none", diff --git a/src/state/settings/types.ts b/src/state/settings/types.ts index e32bd685..c894be9c 100644 --- a/src/state/settings/types.ts +++ b/src/state/settings/types.ts @@ -31,5 +31,6 @@ export interface MWSettingsDataV1 { } export interface MWSettingsData { + language: LangCode; captionSettings: CaptionSettings; } From 2eab07b8b693b63d8a76d8fba4864d9a61cb8a56 Mon Sep 17 00:00:00 2001 From: frost768 Date: Thu, 6 Apr 2023 04:35:20 +0300 Subject: [PATCH 05/23] modal customization --- src/components/layout/Modal.tsx | 2 +- src/views/SettingsModal.tsx | 207 +++++++++++++++++--------------- 2 files changed, 111 insertions(+), 98 deletions(-) diff --git a/src/components/layout/Modal.tsx b/src/components/layout/Modal.tsx index 8ae2f58f..aa25ca7a 100644 --- a/src/components/layout/Modal.tsx +++ b/src/components/layout/Modal.tsx @@ -39,7 +39,7 @@ export function ModalCard(props: { className?: string; children?: ReactNode }) { return (
diff --git a/src/views/SettingsModal.tsx b/src/views/SettingsModal.tsx index b6ad9c19..b05ebe14 100644 --- a/src/views/SettingsModal.tsx +++ b/src/views/SettingsModal.tsx @@ -5,9 +5,9 @@ import { useSettings } from "@/state/settings"; import { useTranslation } from "react-i18next"; import { CaptionCue } from "@/video/components/actions/CaptionRendererAction"; import { Slider } from "@/video/components/popouts/CaptionSettingsPopout"; -import { appLanguageOptions } from "@/setup/i18n"; import { LangCode, captionLanguages } from "@/setup/iso6391"; import { useMemo } from "react"; +import { appLanguageOptions } from "@/setup/i18n"; export default function SettingsModal(props: { onClose: () => void; @@ -15,6 +15,8 @@ export default function SettingsModal(props: { }) { const { captionSettings, + language, + setLanguage, setCaptionLanguage, setCaptionBackgroundColor, setCaptionColor, @@ -34,106 +36,117 @@ export default function SettingsModal(props: { ).toFixed(0); return ( - -
- {t("settings.title")} -
props.onClose()} className="hover:cursor-pointer"> - -
-
-
-
- - l.id === i18n.language)! - } - setSelectedItem={(val) => { - i18n.changeLanguage(val.id); - }} - options={appLanguageOptions} - /> -
-
- - { - setCaptionLanguage(val.id as LangCode); - }} - options={captionLanguages} - /> -
-
-
-
- setCaptionFontSize(e.target.valueAsNumber)} - /> - - setCaptionBackgroundColor(e.target.valueAsNumber) - } - /> -
- -
- {colors.map((color) => ( -
setCaptionColor(color)} - > -
- -
- ))} -
+ +
+
+ {t("settings.title")} +
props.onClose()} + className="hover:cursor-pointer" + > +
-
- {selectedCaptionLanguage.id !== "none" ? ( -
- +
+
+ + l.id === language)! + } + setSelectedItem={(val) => { + i18n.changeLanguage(val.id); + setLanguage(val.id as LangCode); + }} + options={appLanguageOptions} />
- ) : null} +
+ + { + setCaptionLanguage(val.id as LangCode); + }} + options={captionLanguages} + /> +
+
+ setCaptionFontSize(e.target.valueAsNumber)} + /> + + setCaptionBackgroundColor(e.target.valueAsNumber) + } + /> +
+ +
+ {colors.map((color) => ( +
setCaptionColor(color)} + > +
+ +
+ ))} +
+
+
+
+
+
+
+ {selectedCaptionLanguage.id !== "none" ? ( +
+ +
+ ) : null} +
+
From b9b0380dfe75080774cad0fef3b38e8e4282e696 Mon Sep 17 00:00:00 2001 From: frost768 Date: Mon, 10 Apr 2023 00:55:23 +0300 Subject: [PATCH 06/23] suggested changes --- src/components/CaptionColorSelector.tsx | 29 +++++++++ src/components/layout/Modal.tsx | 2 +- src/setup/iso6391.ts | 4 +- .../actions/CaptionRendererAction.tsx | 8 +-- .../popouts/CaptionSettingsPopout.tsx | 29 ++------- src/views/SettingsModal.tsx | 64 +++++++++---------- 6 files changed, 68 insertions(+), 68 deletions(-) create mode 100644 src/components/CaptionColorSelector.tsx diff --git a/src/components/CaptionColorSelector.tsx b/src/components/CaptionColorSelector.tsx new file mode 100644 index 00000000..3d286f78 --- /dev/null +++ b/src/components/CaptionColorSelector.tsx @@ -0,0 +1,29 @@ +import { useSettings } from "@/state/settings"; +import { Icon, Icons } from "./Icon"; + +export const colors = ["#ffffff", "#00ffff", "#ffff00"]; +export default function CaptionColorSelector({ color }: { color: string }) { + const { captionSettings, setCaptionColor } = useSettings(); + return ( +
setCaptionColor(color)} + > +
+ +
+ ); +} diff --git a/src/components/layout/Modal.tsx b/src/components/layout/Modal.tsx index aa25ca7a..d5a910c1 100644 --- a/src/components/layout/Modal.tsx +++ b/src/components/layout/Modal.tsx @@ -40,7 +40,7 @@ export function ModalCard(props: { className?: string; children?: ReactNode }) {
{props.children} diff --git a/src/setup/iso6391.ts b/src/setup/iso6391.ts index 3677560a..0d12f39c 100644 --- a/src/setup/iso6391.ts +++ b/src/setup/iso6391.ts @@ -184,7 +184,7 @@ export type LangCode = | "za" | "zh" | "zu"; -type CaptionLanguageOptions = { +export type CaptionLanguageOption = { id: LangCode; name: string; englishName: string; @@ -212,7 +212,7 @@ type CaptionLanguageOptions = { // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -export const captionLanguages: CaptionLanguageOptions[] = [ +export const captionLanguages: CaptionLanguageOption[] = [ { id: "none", englishName: "None", diff --git a/src/video/components/actions/CaptionRendererAction.tsx b/src/video/components/actions/CaptionRendererAction.tsx index f3a65f52..664cffef 100644 --- a/src/video/components/actions/CaptionRendererAction.tsx +++ b/src/video/components/actions/CaptionRendererAction.tsx @@ -22,14 +22,10 @@ export function CaptionCue({ text, scale }: { text?: string; scale?: number }) { return (

; @@ -16,11 +18,9 @@ export function CaptionSettingsPopout(props: { const { captionSettings, setCaptionBackgroundColor, - setCaptionColor, setCaptionDelay, setCaptionFontSize, } = useSettings(); - const colors = ["#ffffff", "#00ffff", "#ffff00"]; return ( setCaptionDelay(e.target.valueAsNumber)} />

{colors.map((color) => ( -
setCaptionColor(color)} - > -
- -
+ ))}
diff --git a/src/views/SettingsModal.tsx b/src/views/SettingsModal.tsx index b05ebe14..073e03c9 100644 --- a/src/views/SettingsModal.tsx +++ b/src/views/SettingsModal.tsx @@ -4,10 +4,18 @@ import { Modal, ModalCard } from "@/components/layout/Modal"; import { useSettings } from "@/state/settings"; import { useTranslation } from "react-i18next"; import { CaptionCue } from "@/video/components/actions/CaptionRendererAction"; -import { Slider } from "@/video/components/popouts/CaptionSettingsPopout"; -import { LangCode, captionLanguages } from "@/setup/iso6391"; +import { + CaptionLanguageOption, + LangCode, + captionLanguages, +} from "@/setup/iso6391"; import { useMemo } from "react"; import { appLanguageOptions } from "@/setup/i18n"; +import CaptionColorSelector, { + colors, +} from "@/components/CaptionColorSelector"; +import { Slider } from "@/components/Slider"; +import { conf } from "@/setup/config"; export default function SettingsModal(props: { onClose: () => void; @@ -19,16 +27,18 @@ export default function SettingsModal(props: { setLanguage, setCaptionLanguage, setCaptionBackgroundColor, - setCaptionColor, setCaptionFontSize, } = useSettings(); const { t, i18n } = useTranslation(); - const colors = ["#ffffff", "#00ffff", "#ffff00"]; const selectedCaptionLanguage = useMemo( - () => captionLanguages.find((l) => l.id === captionSettings.language)!, + () => captionLanguages.find((l) => l.id === captionSettings.language), [captionSettings.language] - ); + ) as CaptionLanguageOption; + const appLanguage = useMemo( + () => appLanguageOptions.find((l) => l.id === language), + [language] + ) as CaptionLanguageOption; const captionBackgroundOpacity = ( (parseInt(captionSettings.style.backgroundColor.substring(7, 9), 16) / 255) * @@ -54,9 +64,7 @@ export default function SettingsModal(props: { {t("settings.language")} l.id === language)! - } + selectedItem={appLanguage} setSelectedItem={(val) => { i18n.changeLanguage(val.id); setLanguage(val.id as LangCode); @@ -78,7 +86,11 @@ export default function SettingsModal(props: {
setCaptionFontSize(e.target.valueAsNumber)} />
{colors.map((color) => ( -
setCaptionColor(color)} - > -
- -
+ ))}
@@ -149,6 +142,7 @@ export default function SettingsModal(props: {
+
v{conf().APP_VERSION}
); From 546b008b2e2bb7b3063b703d449d2125779ab4c9 Mon Sep 17 00:00:00 2001 From: frost768 Date: Mon, 10 Apr 2023 22:10:11 +0300 Subject: [PATCH 07/23] show text when no caption language is selected --- src/setup/iso6391.ts | 2 +- src/views/SettingsModal.tsx | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/setup/iso6391.ts b/src/setup/iso6391.ts index 0d12f39c..c20580b8 100644 --- a/src/setup/iso6391.ts +++ b/src/setup/iso6391.ts @@ -217,7 +217,7 @@ export const captionLanguages: CaptionLanguageOption[] = [ id: "none", englishName: "None", name: "None", - nativeName: "None", + nativeName: "No caption language selected", }, { id: "aa", diff --git a/src/views/SettingsModal.tsx b/src/views/SettingsModal.tsx index 073e03c9..49f06194 100644 --- a/src/views/SettingsModal.tsx +++ b/src/views/SettingsModal.tsx @@ -130,14 +130,12 @@ export default function SettingsModal(props: {
- {selectedCaptionLanguage.id !== "none" ? ( -
- -
- ) : null} +
+ +
From 84b8a67cea28455e53b3d734204f4845c6f8b1f2 Mon Sep 17 00:00:00 2001 From: Isra Date: Tue, 11 Apr 2023 16:16:06 -0500 Subject: [PATCH 08/23] Time format --- src/video/components/actions/TimeAction.tsx | 48 +++++++++++++++++-- src/video/state/init.ts | 1 + src/video/state/logic/controls.ts | 5 ++ src/video/state/logic/interface.ts | 6 +++ .../state/providers/castingStateProvider.ts | 4 ++ src/video/state/providers/providerTypes.ts | 1 + .../state/providers/videoStateProvider.ts | 4 ++ src/video/state/types.ts | 1 + 8 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/video/components/actions/TimeAction.tsx b/src/video/components/actions/TimeAction.tsx index 493f6d26..1020d093 100644 --- a/src/video/components/actions/TimeAction.tsx +++ b/src/video/components/actions/TimeAction.tsx @@ -1,6 +1,7 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; import { useProgress } from "@/video/state/logic/progress"; +import { useInterface } from "@/video/state/logic/interface"; function durationExceedsHour(secs: number): boolean { return secs > 60 * 60; @@ -37,6 +38,7 @@ export function TimeAction(props: Props) { const descriptor = useVideoPlayerDescriptor(); const videoTime = useProgress(descriptor); const mediaPlaying = useMediaPlaying(descriptor); + const { timeFormat, setTimeFormat } = useInterface(descriptor); const hasHours = durationExceedsHour(videoTime.duration); const time = formatSeconds( @@ -45,11 +47,47 @@ export function TimeAction(props: Props) { ); const duration = formatSeconds(videoTime.duration, hasHours); + const timeLeft = formatSeconds( + (videoTime.duration - videoTime.time) / mediaPlaying.playbackSpeed + ); + + const timeFinished = new Date( + new Date().getTime() + + (videoTime.duration * 1000) / mediaPlaying.playbackSpeed + ).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "numeric", + hour12: true, + }); + return ( -
-

- {time} {props.noDuration ? "" : `/ ${duration}`} -

-
+ ); } diff --git a/src/video/state/init.ts b/src/video/state/init.ts index bd4037fe..6c60ad54 100644 --- a/src/video/state/init.ts +++ b/src/video/state/init.ts @@ -32,6 +32,7 @@ function initPlayer(): VideoPlayerState { isFocused: false, leftControlHovering: false, popoutBounds: null, + timeFormat: 0, }, mediaPlaying: { diff --git a/src/video/state/logic/controls.ts b/src/video/state/logic/controls.ts index e6d33369..535bd146 100644 --- a/src/video/state/logic/controls.ts +++ b/src/video/state/logic/controls.ts @@ -15,6 +15,7 @@ export type ControlMethods = { setDraggingTime(num: number): void; togglePictureInPicture(): void; setPlaybackSpeed(num: number): void; + setTimeFormat(num: 0 | 1): void; }; export function useControls( @@ -110,5 +111,9 @@ export function useControls( state.stateProvider?.setPlaybackSpeed(num); updateInterface(descriptor, state); }, + setTimeFormat(format) { + state.interface.timeFormat = format; + updateInterface(descriptor, state); + }, }; } diff --git a/src/video/state/logic/interface.ts b/src/video/state/logic/interface.ts index 2f22823f..8e761e4e 100644 --- a/src/video/state/logic/interface.ts +++ b/src/video/state/logic/interface.ts @@ -9,6 +9,8 @@ export type VideoInterfaceEvent = { isFocused: boolean; isFullscreen: boolean; popoutBounds: null | DOMRect; + timeFormat: 0 | 1 | 2; + setTimeFormat(timeFormat: 0 | 1 | 2): void; }; function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { @@ -18,6 +20,10 @@ function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { isFocused: state.interface.isFocused, isFullscreen: state.interface.isFullscreen, popoutBounds: state.interface.popoutBounds, + timeFormat: state.interface.timeFormat, + setTimeFormat(timeFormat: 0 | 1 | 2) { + state.stateProvider?.setTimeFormat(timeFormat); + }, }; } diff --git a/src/video/state/providers/castingStateProvider.ts b/src/video/state/providers/castingStateProvider.ts index faf34dc5..e668cebc 100644 --- a/src/video/state/providers/castingStateProvider.ts +++ b/src/video/state/providers/castingStateProvider.ts @@ -173,6 +173,10 @@ export function createCastingStateProvider( updateSource(descriptor, state); } }, + setTimeFormat(format) { + state.interface.timeFormat = format; + updateInterface(descriptor, state); + }, providerStart() { this.setVolume(getStoredVolume()); diff --git a/src/video/state/providers/providerTypes.ts b/src/video/state/providers/providerTypes.ts index ad09e812..6b3d7391 100644 --- a/src/video/state/providers/providerTypes.ts +++ b/src/video/state/providers/providerTypes.ts @@ -23,6 +23,7 @@ export type VideoPlayerStateController = { getId(): string; togglePictureInPicture(): void; setPlaybackSpeed(num: number): void; + setTimeFormat(format: 0 | 1 | 2): void; }; export type VideoPlayerStateProvider = VideoPlayerStateController & { diff --git a/src/video/state/providers/videoStateProvider.ts b/src/video/state/providers/videoStateProvider.ts index e527419b..e8aa9aa3 100644 --- a/src/video/state/providers/videoStateProvider.ts +++ b/src/video/state/providers/videoStateProvider.ts @@ -133,6 +133,10 @@ export function createVideoStateProvider( // update localstorage setStoredVolume(volume); }, + setTimeFormat(num) { + state.interface.timeFormat = num; + updateInterface(descriptor, state); + }, setSource(source) { if (!source) { resetStateForSource(descriptor, state); diff --git a/src/video/state/types.ts b/src/video/state/types.ts index 1ba9ef7a..3a482332 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -30,6 +30,7 @@ export type VideoPlayerState = { isFocused: boolean; // is the video player the users focus? (shortcuts only works when its focused) leftControlHovering: boolean; // is the cursor hovered over the left side of player controls popoutBounds: null | DOMRect; // bounding box of current popout + timeFormat: 0 | 1 | 2; // Time format of the video player }; // state related to the playing state of the media From c330112dbc817ad2607c5366b1e02b4126a3f26a Mon Sep 17 00:00:00 2001 From: Isra Date: Tue, 11 Apr 2023 16:34:19 -0500 Subject: [PATCH 09/23] Translations --- src/setup/locales/en/translation.json | 2 ++ src/setup/locales/fr/translation.json | 5 ++++- src/video/components/actions/TimeAction.tsx | 10 ++++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index ad479bb4..47423b34 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -57,6 +57,8 @@ "backToHome": "Back to home", "backToHomeShort": "Back", "seasonAndEpisode": "S{{season}} E{{episode}}", + "timeLeft": "{{timeLeft}} left", + "finishAt": "Finish at {{timeFinished}}", "buttons": { "episodes": "Episodes", "source": "Source", diff --git a/src/setup/locales/fr/translation.json b/src/setup/locales/fr/translation.json index e5c669ce..fe9d73eb 100644 --- a/src/setup/locales/fr/translation.json +++ b/src/setup/locales/fr/translation.json @@ -39,13 +39,16 @@ "backToHome": "Retour à la page d'accueil", "backToHomeShort": "Retour", "seasonAndEpisode": "S{{season}} E{{episode}}", + "timeLeft": "{{timeLeft}} restant", + "finishAt": "Terminer à {{timeFinished}}", "buttons": { "episodes": "Épisodes", "source": "Source", "captions": "Sous-titres", "download": "Télécharger", "settings": "Paramètres", - "pictureInPicture": "Image dans l'image" + "pictureInPicture": "Image dans l'image", + "playbackSpeed": "Vitesse" }, "popouts": { "sources": "Sources", diff --git a/src/video/components/actions/TimeAction.tsx b/src/video/components/actions/TimeAction.tsx index 1020d093..d355ba2d 100644 --- a/src/video/components/actions/TimeAction.tsx +++ b/src/video/components/actions/TimeAction.tsx @@ -1,4 +1,5 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useTranslation } from "react-i18next"; import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; import { useProgress } from "@/video/state/logic/progress"; import { useInterface } from "@/video/state/logic/interface"; @@ -39,6 +40,7 @@ export function TimeAction(props: Props) { const videoTime = useProgress(descriptor); const mediaPlaying = useMediaPlaying(descriptor); const { timeFormat, setTimeFormat } = useInterface(descriptor); + const { t } = useTranslation(); const hasHours = durationExceedsHour(videoTime.duration); const time = formatSeconds( @@ -80,10 +82,14 @@ export function TimeAction(props: Props) { {/* {time} {props.noDuration ? "" : `/ ${duration}`} */} {timeFormat === 0 ? `${time} ${props.noDuration ? "" : `/ ${duration}`}` - : `${timeLeft} left${ + : `${t("videoPlayer.timeLeft", { + timeLeft, + })}${ videoTime.time === videoTime.duration ? "" - : ` - finish at ${timeFinished}` + : ` - ${t("videoPlayer.finishAt", { + timeFinished, + })}` } `}

From 41fd23cf20595219a67d43e1313a1de514dadab4 Mon Sep 17 00:00:00 2001 From: Isra Date: Fri, 14 Apr 2023 14:11:13 -0500 Subject: [PATCH 10/23] Reviews --- src/video/components/actions/TimeAction.tsx | 54 +++++++++++++-------- src/video/state/logic/controls.ts | 4 +- src/video/state/logic/interface.ts | 8 +-- src/video/state/providers/providerTypes.ts | 3 +- src/video/state/types.ts | 7 ++- 5 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/video/components/actions/TimeAction.tsx b/src/video/components/actions/TimeAction.tsx index d355ba2d..d71d345c 100644 --- a/src/video/components/actions/TimeAction.tsx +++ b/src/video/components/actions/TimeAction.tsx @@ -3,6 +3,9 @@ import { useTranslation } from "react-i18next"; import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; import { useProgress } from "@/video/state/logic/progress"; import { useInterface } from "@/video/state/logic/interface"; +import { VideoPlayerTimeFormat } from "@/video/state/types"; +import { useIsMobile } from "@/hooks/useIsMobile"; +import { useControls } from "@/video/state/logic/controls"; function durationExceedsHour(secs: number): boolean { return secs > 60 * 60; @@ -40,19 +43,20 @@ export function TimeAction(props: Props) { const videoTime = useProgress(descriptor); const mediaPlaying = useMediaPlaying(descriptor); const { timeFormat, setTimeFormat } = useInterface(descriptor); + const { isMobile } = useIsMobile(); const { t } = useTranslation(); const hasHours = durationExceedsHour(videoTime.duration); - const time = formatSeconds( + + const currentTime = formatSeconds( mediaPlaying.isDragSeeking ? videoTime.draggingTime : videoTime.time, hasHours ); const duration = formatSeconds(videoTime.duration, hasHours); - const timeLeft = formatSeconds( - (videoTime.duration - videoTime.time) / mediaPlaying.playbackSpeed + (videoTime.duration - videoTime.time) / mediaPlaying.playbackSpeed, + hasHours ); - const timeFinished = new Date( new Date().getTime() + (videoTime.duration * 1000) / mediaPlaying.playbackSpeed @@ -61,15 +65,38 @@ export function TimeAction(props: Props) { minute: "numeric", hour12: true, }); + const formattedTimeFinished = ` - ${t("videoPlayer.finishAt", { + timeFinished, + })}`; + + let formattedTime: string; + + if (timeFormat === VideoPlayerTimeFormat.REGULAR) { + formattedTime = `${currentTime} ${props.noDuration ? "" : `/ ${duration}`}`; + } else if (timeFormat === VideoPlayerTimeFormat.REMAINING) { + formattedTime = `${t("videoPlayer.timeLeft", { + timeLeft, + })}${ + videoTime.time === videoTime.duration || isMobile + ? "" + : formattedTimeFinished + } `; + } else { + formattedTime = ""; + } return ( diff --git a/src/video/state/logic/controls.ts b/src/video/state/logic/controls.ts index 535bd146..ccd83add 100644 --- a/src/video/state/logic/controls.ts +++ b/src/video/state/logic/controls.ts @@ -1,7 +1,7 @@ import { updateInterface } from "@/video/state/logic/interface"; import { updateMeta } from "@/video/state/logic/meta"; import { updateProgress } from "@/video/state/logic/progress"; -import { VideoPlayerMeta } from "@/video/state/types"; +import { VideoPlayerMeta, VideoPlayerTimeFormat } from "@/video/state/types"; import { getPlayerState } from "../cache"; import { VideoPlayerStateController } from "../providers/providerTypes"; @@ -15,7 +15,7 @@ export type ControlMethods = { setDraggingTime(num: number): void; togglePictureInPicture(): void; setPlaybackSpeed(num: number): void; - setTimeFormat(num: 0 | 1): void; + setTimeFormat(num: VideoPlayerTimeFormat): void; }; export function useControls( diff --git a/src/video/state/logic/interface.ts b/src/video/state/logic/interface.ts index 8e761e4e..5a5ea719 100644 --- a/src/video/state/logic/interface.ts +++ b/src/video/state/logic/interface.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { getPlayerState } from "../cache"; import { listenEvent, sendEvent, unlistenEvent } from "../events"; -import { VideoPlayerState } from "../types"; +import { VideoPlayerState, VideoPlayerTimeFormat } from "../types"; export type VideoInterfaceEvent = { popout: string | null; @@ -9,8 +9,8 @@ export type VideoInterfaceEvent = { isFocused: boolean; isFullscreen: boolean; popoutBounds: null | DOMRect; - timeFormat: 0 | 1 | 2; - setTimeFormat(timeFormat: 0 | 1 | 2): void; + timeFormat: VideoPlayerTimeFormat; + setTimeFormat(timeFormat: VideoPlayerTimeFormat): void; }; function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { @@ -21,7 +21,7 @@ function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { isFullscreen: state.interface.isFullscreen, popoutBounds: state.interface.popoutBounds, timeFormat: state.interface.timeFormat, - setTimeFormat(timeFormat: 0 | 1 | 2) { + setTimeFormat(timeFormat: VideoPlayerTimeFormat) { state.stateProvider?.setTimeFormat(timeFormat); }, }; diff --git a/src/video/state/providers/providerTypes.ts b/src/video/state/providers/providerTypes.ts index 6b3d7391..2cab148b 100644 --- a/src/video/state/providers/providerTypes.ts +++ b/src/video/state/providers/providerTypes.ts @@ -1,4 +1,5 @@ import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; +import { VideoPlayerTimeFormat } from "@/video/state/types"; type VideoPlayerSource = { source: string; @@ -23,7 +24,7 @@ export type VideoPlayerStateController = { getId(): string; togglePictureInPicture(): void; setPlaybackSpeed(num: number): void; - setTimeFormat(format: 0 | 1 | 2): void; + setTimeFormat(timeFormat: VideoPlayerTimeFormat): void; }; export type VideoPlayerStateProvider = VideoPlayerStateController & { diff --git a/src/video/state/types.ts b/src/video/state/types.ts index 3a482332..e5e403da 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -22,6 +22,11 @@ export type VideoPlayerMeta = { }[]; }; +export enum VideoPlayerTimeFormat { + REGULAR = 0, + REMAINING = 1, +} + export type VideoPlayerState = { // state related to the user interface interface: { @@ -30,7 +35,7 @@ export type VideoPlayerState = { isFocused: boolean; // is the video player the users focus? (shortcuts only works when its focused) leftControlHovering: boolean; // is the cursor hovered over the left side of player controls popoutBounds: null | DOMRect; // bounding box of current popout - timeFormat: 0 | 1 | 2; // Time format of the video player + timeFormat: VideoPlayerTimeFormat; // Time format of the video player }; // state related to the playing state of the media From c5251401e702a6772a9fa8a793ef18fe27fe573b Mon Sep 17 00:00:00 2001 From: Isra Date: Fri, 14 Apr 2023 14:18:17 -0500 Subject: [PATCH 11/23] Does this fix it? --- src/video/components/actions/TimeAction.tsx | 3 ++- src/video/state/logic/interface.ts | 5 +---- src/video/state/providers/castingStateProvider.ts | 4 ---- src/video/state/providers/providerTypes.ts | 2 -- src/video/state/providers/videoStateProvider.ts | 4 ---- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/video/components/actions/TimeAction.tsx b/src/video/components/actions/TimeAction.tsx index d71d345c..184256ea 100644 --- a/src/video/components/actions/TimeAction.tsx +++ b/src/video/components/actions/TimeAction.tsx @@ -42,7 +42,8 @@ export function TimeAction(props: Props) { const descriptor = useVideoPlayerDescriptor(); const videoTime = useProgress(descriptor); const mediaPlaying = useMediaPlaying(descriptor); - const { timeFormat, setTimeFormat } = useInterface(descriptor); + const { setTimeFormat } = useControls(descriptor); + const { timeFormat } = useInterface(descriptor); const { isMobile } = useIsMobile(); const { t } = useTranslation(); diff --git a/src/video/state/logic/interface.ts b/src/video/state/logic/interface.ts index 5a5ea719..6d0357ab 100644 --- a/src/video/state/logic/interface.ts +++ b/src/video/state/logic/interface.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useControls } from "@/video/state/logic/controls"; import { getPlayerState } from "../cache"; import { listenEvent, sendEvent, unlistenEvent } from "../events"; import { VideoPlayerState, VideoPlayerTimeFormat } from "../types"; @@ -10,7 +11,6 @@ export type VideoInterfaceEvent = { isFullscreen: boolean; popoutBounds: null | DOMRect; timeFormat: VideoPlayerTimeFormat; - setTimeFormat(timeFormat: VideoPlayerTimeFormat): void; }; function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { @@ -21,9 +21,6 @@ function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { isFullscreen: state.interface.isFullscreen, popoutBounds: state.interface.popoutBounds, timeFormat: state.interface.timeFormat, - setTimeFormat(timeFormat: VideoPlayerTimeFormat) { - state.stateProvider?.setTimeFormat(timeFormat); - }, }; } diff --git a/src/video/state/providers/castingStateProvider.ts b/src/video/state/providers/castingStateProvider.ts index e668cebc..faf34dc5 100644 --- a/src/video/state/providers/castingStateProvider.ts +++ b/src/video/state/providers/castingStateProvider.ts @@ -173,10 +173,6 @@ export function createCastingStateProvider( updateSource(descriptor, state); } }, - setTimeFormat(format) { - state.interface.timeFormat = format; - updateInterface(descriptor, state); - }, providerStart() { this.setVolume(getStoredVolume()); diff --git a/src/video/state/providers/providerTypes.ts b/src/video/state/providers/providerTypes.ts index 2cab148b..ad09e812 100644 --- a/src/video/state/providers/providerTypes.ts +++ b/src/video/state/providers/providerTypes.ts @@ -1,5 +1,4 @@ import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; -import { VideoPlayerTimeFormat } from "@/video/state/types"; type VideoPlayerSource = { source: string; @@ -24,7 +23,6 @@ export type VideoPlayerStateController = { getId(): string; togglePictureInPicture(): void; setPlaybackSpeed(num: number): void; - setTimeFormat(timeFormat: VideoPlayerTimeFormat): void; }; export type VideoPlayerStateProvider = VideoPlayerStateController & { diff --git a/src/video/state/providers/videoStateProvider.ts b/src/video/state/providers/videoStateProvider.ts index e8aa9aa3..e527419b 100644 --- a/src/video/state/providers/videoStateProvider.ts +++ b/src/video/state/providers/videoStateProvider.ts @@ -133,10 +133,6 @@ export function createVideoStateProvider( // update localstorage setStoredVolume(volume); }, - setTimeFormat(num) { - state.interface.timeFormat = num; - updateInterface(descriptor, state); - }, setSource(source) { if (!source) { resetStateForSource(descriptor, state); From e52b29a1a1809f6ffd451cbb20a75c4061d8b1f9 Mon Sep 17 00:00:00 2001 From: JORDAAR <69628820+Jordaar@users.noreply.github.com> Date: Wed, 19 Apr 2023 15:44:20 +0530 Subject: [PATCH 12/23] add hdwatched provider --- src/backend/index.ts | 1 + src/backend/providers/hdwatched.ts | 105 +++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/backend/providers/hdwatched.ts diff --git a/src/backend/index.ts b/src/backend/index.ts index 7a13a445..5cf50906 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -6,6 +6,7 @@ import "./providers/flixhq"; import "./providers/superstream"; import "./providers/netfilm"; import "./providers/m4ufree"; +import "./providers/hdwatched"; // embeds import "./embeds/streamm4u"; diff --git a/src/backend/providers/hdwatched.ts b/src/backend/providers/hdwatched.ts new file mode 100644 index 00000000..08a3935b --- /dev/null +++ b/src/backend/providers/hdwatched.ts @@ -0,0 +1,105 @@ +import { proxiedFetch } from "../helpers/fetch"; +import { registerProvider } from "../helpers/register"; +import { MWStreamQuality, MWStreamType } from "../helpers/streams"; +import { MWMediaType } from "../metadata/types"; + +const hdwatchedBase = "https://www.hdwatched.xyz"; + +const qualityMap: Record = { + 360: MWStreamQuality.Q360P, + 540: MWStreamQuality.Q540P, + 480: MWStreamQuality.Q480P, + 720: MWStreamQuality.Q720P, + 1080: MWStreamQuality.Q1080P, +}; + +interface MovieSearchList { + title: string; + id: string; + year: number; +} + +registerProvider({ + id: "hdwatched", + displayName: "HDwatched", + rank: 50, + type: [MWMediaType.MOVIE], + async scrape({ media, progress }) { + if (!this.type.includes(media.meta.type)) { + throw new Error("Unsupported type"); + } + + progress(20); + + const search = await proxiedFetch(`/search/${media.imdbId}`, { + baseURL: hdwatchedBase, + }); + + const searchPage = new DOMParser().parseFromString(search, "text/html"); + const movieElements = searchPage.querySelectorAll("div.i-container"); + + const movieSearchList: MovieSearchList[] = []; + movieElements.forEach((movieElement) => { + const href = movieElement.querySelector("a")?.getAttribute("href") || ""; + const title = + movieElement?.querySelector("span.content-title")?.textContent || ""; + const year = + parseInt( + movieElement + ?.querySelector("div.duration") + ?.textContent?.trim() + ?.split(" ") + ?.pop() || "", + 10 + ) || 0; + + movieSearchList.push({ + title, + year, + id: href.split("/")[2], // Format: /free/{id}}/{movie-slug} | Example: /free/18804/iron-man-231 + }); + }); + + progress(50); + + const targetMovie = movieSearchList.find( + (movie) => movie.year === (media.meta.year ? +media.meta.year : 0) // Compare year to make the search more robust + ); + + if (!targetMovie) { + throw new Error("Could not find stream"); + } + + const stream = await proxiedFetch(`/embed/${targetMovie.id}`, { + baseURL: hdwatchedBase, + }); + + progress(80); + + const embedPage = new DOMParser().parseFromString(stream, "text/html"); + const source = embedPage.querySelector("#vjsplayer > source"); + if (!source) { + throw new Error("Could not find stream"); + } + + const streamSrc = source.getAttribute("src"); + const streamRes = source.getAttribute("res"); + + if (!streamSrc) { + throw new Error("Could not find stream"); + } + + return { + embeds: [], + stream: { + streamUrl: streamSrc, + quality: + streamRes && typeof +streamRes === "number" + ? qualityMap[+streamRes] + : MWStreamQuality.QUNKNOWN, + type: MWStreamType.MP4, + captions: [], + }, + }; + }, +}); From 7b75c36d2149a977058b2df820eb04ff9d68419d Mon Sep 17 00:00:00 2001 From: JORDAAR <69628820+Jordaar@users.noreply.github.com> Date: Thu, 20 Apr 2023 15:53:28 +0530 Subject: [PATCH 13/23] add series support & improvements --- src/backend/providers/hdwatched.ts | 169 ++++++++++++++++++++++------- 1 file changed, 130 insertions(+), 39 deletions(-) diff --git a/src/backend/providers/hdwatched.ts b/src/backend/providers/hdwatched.ts index 08a3935b..65e0c356 100644 --- a/src/backend/providers/hdwatched.ts +++ b/src/backend/providers/hdwatched.ts @@ -1,4 +1,5 @@ import { proxiedFetch } from "../helpers/fetch"; +import { MWProviderContext } from "../helpers/provider"; import { registerProvider } from "../helpers/register"; import { MWStreamQuality, MWStreamType } from "../helpers/streams"; import { MWMediaType } from "../metadata/types"; @@ -13,39 +14,134 @@ const qualityMap: Record = { 1080: MWStreamQuality.Q1080P, }; -interface MovieSearchList { +interface SearchRes { title: string; + year?: number; + href: string; id: string; - year: number; +} + +function getStreamFromEmbed(stream: string) { + const embedPage = new DOMParser().parseFromString(stream, "text/html"); + const source = embedPage.querySelector("#vjsplayer > source"); + if (!source) { + throw new Error("Unable to fetch stream"); + } + + const streamSrc = source.getAttribute("src"); + const streamRes = source.getAttribute("res"); + + if (!streamSrc || !streamRes) throw new Error("Unable to find stream"); + + return { + streamUrl: streamSrc, + quality: + streamRes && typeof +streamRes === "number" + ? qualityMap[+streamRes] + : MWStreamQuality.QUNKNOWN, + }; +} + +async function fetchMovie(targetSource: SearchRes) { + const stream = await proxiedFetch(`/embed/${targetSource.id}`, { + baseURL: hdwatchedBase, + }); + + const embedPage = new DOMParser().parseFromString(stream, "text/html"); + const source = embedPage.querySelector("#vjsplayer > source"); + if (!source) { + throw new Error("Unable to fetch movie stream"); + } + + return getStreamFromEmbed(stream); +} + +async function fetchSeries( + targetSource: SearchRes, + { media, episode, progress }: MWProviderContext +) { + if (media.meta.type !== MWMediaType.SERIES) + throw new Error("Media type mismatch"); + + const seasonNumber = media.meta.seasonData.number; + const episodeNumber = media.meta.seasonData.episodes.find( + (e) => e.id === episode + )?.number; + + if (!seasonNumber || !episodeNumber) + throw new Error("Unable to get season or episode number"); + + const seriesPage = await proxiedFetch( + `${targetSource.href}?season=${media.meta.seasonData.number}`, + { + baseURL: hdwatchedBase, + } + ); + + const seasonPage = new DOMParser().parseFromString(seriesPage, "text/html"); + const pageElements = seasonPage.querySelectorAll("div.i-container"); + + const seriesList: SearchRes[] = []; + pageElements.forEach((pageElement) => { + const href = pageElement.querySelector("a")?.getAttribute("href") || ""; + const title = + pageElement?.querySelector("span.content-title")?.textContent || ""; + + seriesList.push({ + title, + href, + id: href.split("/")[2], // Format: /free/{id}/{series-slug}-season-{season-number}-episode-{episode-number} + }); + }); + + const targetEpisode = seriesList.find( + (episodeEl) => + episodeEl.title.trim().toLowerCase() === `episode ${episodeNumber}` + ); + + if (!targetEpisode) throw new Error("Unable to find episode"); + + progress(70); + + const stream = await proxiedFetch(`/embed/${targetEpisode.id}`, { + baseURL: hdwatchedBase, + }); + + const embedPage = new DOMParser().parseFromString(stream, "text/html"); + const source = embedPage.querySelector("#vjsplayer > source"); + if (!source) { + throw new Error("Unable to fetch movie stream"); + } + + return getStreamFromEmbed(stream); } registerProvider({ id: "hdwatched", displayName: "HDwatched", rank: 50, - type: [MWMediaType.MOVIE], - async scrape({ media, progress }) { + type: [MWMediaType.MOVIE, MWMediaType.SERIES], + async scrape(options) { + const { media, progress } = options; if (!this.type.includes(media.meta.type)) { throw new Error("Unsupported type"); } - progress(20); - const search = await proxiedFetch(`/search/${media.imdbId}`, { baseURL: hdwatchedBase, }); const searchPage = new DOMParser().parseFromString(search, "text/html"); - const movieElements = searchPage.querySelectorAll("div.i-container"); + const pageElements = searchPage.querySelectorAll("div.i-container"); - const movieSearchList: MovieSearchList[] = []; - movieElements.forEach((movieElement) => { - const href = movieElement.querySelector("a")?.getAttribute("href") || ""; + const searchList: SearchRes[] = []; + pageElements.forEach((pageElement) => { + const href = pageElement.querySelector("a")?.getAttribute("href") || ""; const title = - movieElement?.querySelector("span.content-title")?.textContent || ""; + pageElement?.querySelector("span.content-title")?.textContent || ""; const year = parseInt( - movieElement + pageElement ?.querySelector("div.duration") ?.textContent?.trim() ?.split(" ") @@ -53,50 +149,45 @@ registerProvider({ 10 ) || 0; - movieSearchList.push({ + searchList.push({ title, year, - id: href.split("/")[2], // Format: /free/{id}}/{movie-slug} | Example: /free/18804/iron-man-231 + href, + id: href.split("/")[2], // Format: /free/{id}/{movie-slug} or /series/{id}/{series-slug} }); }); - progress(50); + progress(20); - const targetMovie = movieSearchList.find( - (movie) => movie.year === (media.meta.year ? +media.meta.year : 0) // Compare year to make the search more robust + const targetSource = searchList.find( + (source) => source.year === (media.meta.year ? +media.meta.year : 0) // Compare year to make the search more robust ); - if (!targetMovie) { + if (!targetSource) { throw new Error("Could not find stream"); } - const stream = await proxiedFetch(`/embed/${targetMovie.id}`, { - baseURL: hdwatchedBase, - }); + progress(40); - progress(80); - - const embedPage = new DOMParser().parseFromString(stream, "text/html"); - const source = embedPage.querySelector("#vjsplayer > source"); - if (!source) { - throw new Error("Could not find stream"); - } - - const streamSrc = source.getAttribute("src"); - const streamRes = source.getAttribute("res"); - - if (!streamSrc) { - throw new Error("Could not find stream"); + if (media.meta.type === MWMediaType.SERIES) { + const series = await fetchSeries(targetSource, options); + return { + embeds: [], + stream: { + streamUrl: series.streamUrl, + quality: series.quality, + type: MWStreamType.MP4, + captions: [], + }, + }; } + const movie = await fetchMovie(targetSource); return { embeds: [], stream: { - streamUrl: streamSrc, - quality: - streamRes && typeof +streamRes === "number" - ? qualityMap[+streamRes] - : MWStreamQuality.QUNKNOWN, + streamUrl: movie.streamUrl, + quality: movie.quality, type: MWStreamType.MP4, captions: [], }, From b26b0715bd913e144cdb4a463e17d9fc1933b4c2 Mon Sep 17 00:00:00 2001 From: JORDAAR <69628820+Jordaar@users.noreply.github.com> Date: Thu, 20 Apr 2023 22:26:54 +0530 Subject: [PATCH 14/23] increase rank --- src/backend/providers/hdwatched.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/providers/hdwatched.ts b/src/backend/providers/hdwatched.ts index 65e0c356..cacaec41 100644 --- a/src/backend/providers/hdwatched.ts +++ b/src/backend/providers/hdwatched.ts @@ -119,7 +119,7 @@ async function fetchSeries( registerProvider({ id: "hdwatched", displayName: "HDwatched", - rank: 50, + rank: 150, type: [MWMediaType.MOVIE, MWMediaType.SERIES], async scrape(options) { const { media, progress } = options; From 0c2df2cd3cbb866e92a8fdb500ef10818b932c59 Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Thu, 20 Apr 2023 19:50:57 +0200 Subject: [PATCH 15/23] fix(player): fix dismissal of UI after only 1 mousemove event --- src/video/components/actions/BackdropAction.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/video/components/actions/BackdropAction.tsx b/src/video/components/actions/BackdropAction.tsx index d7e75605..e0a1f9eb 100644 --- a/src/video/components/actions/BackdropAction.tsx +++ b/src/video/components/actions/BackdropAction.tsx @@ -24,18 +24,16 @@ export function BackdropAction(props: BackdropActionProps) { const handleMouseMove = useCallback(() => { if (!moved) { setTimeout(() => { + // If NOT a touch, set moved to true const isTouch = Date.now() - lastTouchEnd.current < 200; - if (!isTouch) { - setMoved(true); - } + if (!isTouch) setMoved(true); }, 20); - return; } // remove after all if (timeout.current) clearTimeout(timeout.current); timeout.current = setTimeout(() => { - if (moved) setMoved(false); + setMoved(false); timeout.current = null; }, 3000); }, [setMoved, moved]); From 2239c186a585933e797f1ac6542ed67b8fe89da6 Mon Sep 17 00:00:00 2001 From: frost768 Date: Thu, 20 Apr 2023 21:43:51 +0300 Subject: [PATCH 16/23] modal background changed --- src/components/layout/Modal.tsx | 2 +- src/views/SettingsModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/layout/Modal.tsx b/src/components/layout/Modal.tsx index d5a910c1..b3e7a22e 100644 --- a/src/components/layout/Modal.tsx +++ b/src/components/layout/Modal.tsx @@ -39,7 +39,7 @@ export function ModalCard(props: { className?: string; children?: ReactNode }) { return (
diff --git a/src/views/SettingsModal.tsx b/src/views/SettingsModal.tsx index 49f06194..b3ba74ed 100644 --- a/src/views/SettingsModal.tsx +++ b/src/views/SettingsModal.tsx @@ -46,7 +46,7 @@ export default function SettingsModal(props: { ).toFixed(0); return ( - +
{t("settings.title")} From 2424cdfc9e8eeb3288b6f7c47f402e7fe8447649 Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Thu, 20 Apr 2023 20:51:05 +0200 Subject: [PATCH 17/23] feat(video): add "volume adjusted" bar on top for keyboard events --- src/video/components/VideoPlayer.tsx | 2 ++ .../actions/KeyboardShortcutsAction.tsx | 4 +-- .../actions/VolumeAdjustedAction.tsx | 33 +++++++++++++++++++ src/video/state/init.ts | 1 + src/video/state/logic/controls.ts | 18 ++++++++-- src/video/state/logic/interface.ts | 2 ++ src/video/state/providers/providerTypes.ts | 2 +- src/video/state/types.ts | 1 + 8 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 src/video/components/actions/VolumeAdjustedAction.tsx diff --git a/src/video/components/VideoPlayer.tsx b/src/video/components/VideoPlayer.tsx index 22d96502..8e5888a1 100644 --- a/src/video/components/VideoPlayer.tsx +++ b/src/video/components/VideoPlayer.tsx @@ -31,6 +31,7 @@ import { PictureInPictureAction } from "@/video/components/actions/PictureInPict import { CaptionRendererAction } from "./actions/CaptionRendererAction"; import { SettingsAction } from "./actions/SettingsAction"; import { DividerAction } from "./actions/DividerAction"; +import { VolumeAdjustedAction } from "./actions/VolumeAdjustedAction"; type Props = VideoPlayerBaseProps; @@ -91,6 +92,7 @@ export function VideoPlayer(props: Props) { <> + diff --git a/src/video/components/actions/KeyboardShortcutsAction.tsx b/src/video/components/actions/KeyboardShortcutsAction.tsx index 24e8b813..ba5ffc32 100644 --- a/src/video/components/actions/KeyboardShortcutsAction.tsx +++ b/src/video/components/actions/KeyboardShortcutsAction.tsx @@ -65,12 +65,12 @@ export function KeyboardShortcutsAction() { // Decrease volume case "arrowdown": - controls.setVolume(Math.max(mediaPlaying.volume - 0.1, 0)); + controls.setVolume(Math.max(mediaPlaying.volume - 0.1, 0), true); break; // Increase volume case "arrowup": - controls.setVolume(Math.min(mediaPlaying.volume + 0.1, 1)); + controls.setVolume(Math.min(mediaPlaying.volume + 0.1, 1), true); break; // Do a barrel Roll! diff --git a/src/video/components/actions/VolumeAdjustedAction.tsx b/src/video/components/actions/VolumeAdjustedAction.tsx new file mode 100644 index 00000000..29a58da1 --- /dev/null +++ b/src/video/components/actions/VolumeAdjustedAction.tsx @@ -0,0 +1,33 @@ +import { Icon, Icons } from "@/components/Icon"; +import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useControls } from "@/video/state/logic/controls"; +import { useInterface } from "@/video/state/logic/interface"; +import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; + +export function VolumeAdjustedAction() { + const descriptor = useVideoPlayerDescriptor(); + const videoInterface = useInterface(descriptor); + const mediaPlaying = useMediaPlaying(descriptor); + + return ( +
+ 0 ? Icons.VOLUME : Icons.VOLUME_X} + className="text-xl text-white" + /> +
+
+
+
+ ); +} diff --git a/src/video/state/init.ts b/src/video/state/init.ts index bd4037fe..3c55a642 100644 --- a/src/video/state/init.ts +++ b/src/video/state/init.ts @@ -32,6 +32,7 @@ function initPlayer(): VideoPlayerState { isFocused: false, leftControlHovering: false, popoutBounds: null, + volumeChangedWithKeybind: false, }, mediaPlaying: { diff --git a/src/video/state/logic/controls.ts b/src/video/state/logic/controls.ts index e6d33369..fc8f99e5 100644 --- a/src/video/state/logic/controls.ts +++ b/src/video/state/logic/controls.ts @@ -5,6 +5,8 @@ import { VideoPlayerMeta } from "@/video/state/types"; import { getPlayerState } from "../cache"; import { VideoPlayerStateController } from "../providers/providerTypes"; +let volumeChangedWithKeybindDebounce: NodeJS.Timeout | null = null; + export type ControlMethods = { openPopout(id: string): void; closePopout(): void; @@ -48,8 +50,20 @@ export function useControls( enterFullscreen() { state.stateProvider?.enterFullscreen(); }, - setVolume(volume) { - state.stateProvider?.setVolume(volume); + setVolume(volume, isKeyboardEvent = false) { + if (isKeyboardEvent) { + if (volumeChangedWithKeybindDebounce) + clearTimeout(volumeChangedWithKeybindDebounce); + + state.interface.volumeChangedWithKeybind = true; + updateInterface(descriptor, state); + + volumeChangedWithKeybindDebounce = setTimeout(() => { + state.interface.volumeChangedWithKeybind = false; + updateInterface(descriptor, state); + }, 3e3); + } + state.stateProvider?.setVolume(volume, isKeyboardEvent); }, startAirplay() { state.stateProvider?.startAirplay(); diff --git a/src/video/state/logic/interface.ts b/src/video/state/logic/interface.ts index 2f22823f..43185a3a 100644 --- a/src/video/state/logic/interface.ts +++ b/src/video/state/logic/interface.ts @@ -9,6 +9,7 @@ export type VideoInterfaceEvent = { isFocused: boolean; isFullscreen: boolean; popoutBounds: null | DOMRect; + volumeChangedWithKeybind: boolean; }; function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { @@ -18,6 +19,7 @@ function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { isFocused: state.interface.isFocused, isFullscreen: state.interface.isFullscreen, popoutBounds: state.interface.popoutBounds, + volumeChangedWithKeybind: state.interface.volumeChangedWithKeybind, }; } diff --git a/src/video/state/providers/providerTypes.ts b/src/video/state/providers/providerTypes.ts index ad09e812..acc73dc5 100644 --- a/src/video/state/providers/providerTypes.ts +++ b/src/video/state/providers/providerTypes.ts @@ -16,7 +16,7 @@ export type VideoPlayerStateController = { setSeeking(active: boolean): void; exitFullscreen(): void; enterFullscreen(): void; - setVolume(volume: number): void; + setVolume(volume: number, isKeyboardEvent?: boolean): void; startAirplay(): void; setCaption(id: string, url: string): void; clearCaption(): void; diff --git a/src/video/state/types.ts b/src/video/state/types.ts index 1ba9ef7a..be6016c4 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -28,6 +28,7 @@ export type VideoPlayerState = { isFullscreen: boolean; popout: string | null; // id of current popout (eg source select, episode select) isFocused: boolean; // is the video player the users focus? (shortcuts only works when its focused) + volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently? leftControlHovering: boolean; // is the cursor hovered over the left side of player controls popoutBounds: null | DOMRect; // bounding box of current popout }; From 1472b21600212078d8de825b525abe7f1a9e1475 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 20 Apr 2023 20:52:06 +0200 Subject: [PATCH 18/23] negative sign thingy --- src/video/components/actions/TimeAction.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/video/components/actions/TimeAction.tsx b/src/video/components/actions/TimeAction.tsx index 184256ea..bab9e101 100644 --- a/src/video/components/actions/TimeAction.tsx +++ b/src/video/components/actions/TimeAction.tsx @@ -74,14 +74,12 @@ export function TimeAction(props: Props) { if (timeFormat === VideoPlayerTimeFormat.REGULAR) { formattedTime = `${currentTime} ${props.noDuration ? "" : `/ ${duration}`}`; - } else if (timeFormat === VideoPlayerTimeFormat.REMAINING) { + } else if (timeFormat === VideoPlayerTimeFormat.REMAINING && !isMobile) { formattedTime = `${t("videoPlayer.timeLeft", { timeLeft, - })}${ - videoTime.time === videoTime.duration || isMobile - ? "" - : formattedTimeFinished - } `; + })}${videoTime.time === videoTime.duration ? "" : formattedTimeFinished} `; + } else if (timeFormat === VideoPlayerTimeFormat.REMAINING && isMobile) { + formattedTime = `-${timeLeft}`; } else { formattedTime = ""; } From 43c8da9003b0e2d24babb044b39dfa16cbf80af2 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 20 Apr 2023 20:53:23 +0200 Subject: [PATCH 19/23] remove unsused useControls --- src/video/state/logic/interface.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/video/state/logic/interface.ts b/src/video/state/logic/interface.ts index 6d0357ab..4ac47a3a 100644 --- a/src/video/state/logic/interface.ts +++ b/src/video/state/logic/interface.ts @@ -1,5 +1,4 @@ import { useEffect, useState } from "react"; -import { useControls } from "@/video/state/logic/controls"; import { getPlayerState } from "../cache"; import { listenEvent, sendEvent, unlistenEvent } from "../events"; import { VideoPlayerState, VideoPlayerTimeFormat } from "../types"; From a0a51c898a618af4ac380733bfd0e2dc2325d6f0 Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Thu, 20 Apr 2023 20:53:35 +0200 Subject: [PATCH 20/23] chore: remove unused import --- src/video/components/actions/VolumeAdjustedAction.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/video/components/actions/VolumeAdjustedAction.tsx b/src/video/components/actions/VolumeAdjustedAction.tsx index 29a58da1..a5c547c5 100644 --- a/src/video/components/actions/VolumeAdjustedAction.tsx +++ b/src/video/components/actions/VolumeAdjustedAction.tsx @@ -1,6 +1,5 @@ import { Icon, Icons } from "@/components/Icon"; import { useVideoPlayerDescriptor } from "@/video/state/hooks"; -import { useControls } from "@/video/state/logic/controls"; import { useInterface } from "@/video/state/logic/interface"; import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; From 7007f030e19572db271c83cdd0a086e0d08094cc Mon Sep 17 00:00:00 2001 From: Jip Fr Date: Thu, 20 Apr 2023 21:07:44 +0200 Subject: [PATCH 21/23] feat(player): use state-specific debouncer, not global --- src/video/state/init.ts | 1 + src/video/state/logic/controls.ts | 8 +++----- src/video/state/types.ts | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/video/state/init.ts b/src/video/state/init.ts index 3c55a642..570d27d3 100644 --- a/src/video/state/init.ts +++ b/src/video/state/init.ts @@ -33,6 +33,7 @@ function initPlayer(): VideoPlayerState { leftControlHovering: false, popoutBounds: null, volumeChangedWithKeybind: false, + volumeChangedWithKeybindDebounce: null, }, mediaPlaying: { diff --git a/src/video/state/logic/controls.ts b/src/video/state/logic/controls.ts index fc8f99e5..76cc8e2e 100644 --- a/src/video/state/logic/controls.ts +++ b/src/video/state/logic/controls.ts @@ -5,8 +5,6 @@ import { VideoPlayerMeta } from "@/video/state/types"; import { getPlayerState } from "../cache"; import { VideoPlayerStateController } from "../providers/providerTypes"; -let volumeChangedWithKeybindDebounce: NodeJS.Timeout | null = null; - export type ControlMethods = { openPopout(id: string): void; closePopout(): void; @@ -52,13 +50,13 @@ export function useControls( }, setVolume(volume, isKeyboardEvent = false) { if (isKeyboardEvent) { - if (volumeChangedWithKeybindDebounce) - clearTimeout(volumeChangedWithKeybindDebounce); + if (state.interface.volumeChangedWithKeybindDebounce) + clearTimeout(state.interface.volumeChangedWithKeybindDebounce); state.interface.volumeChangedWithKeybind = true; updateInterface(descriptor, state); - volumeChangedWithKeybindDebounce = setTimeout(() => { + state.interface.volumeChangedWithKeybindDebounce = setTimeout(() => { state.interface.volumeChangedWithKeybind = false; updateInterface(descriptor, state); }, 3e3); diff --git a/src/video/state/types.ts b/src/video/state/types.ts index be6016c4..fc059979 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -29,6 +29,7 @@ export type VideoPlayerState = { popout: string | null; // id of current popout (eg source select, episode select) isFocused: boolean; // is the video player the users focus? (shortcuts only works when its focused) volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently? + volumeChangedWithKeybindDebounce: NodeJS.Timeout | null; // debounce for the duration of the "volume changed thingamajig" leftControlHovering: boolean; // is the cursor hovered over the left side of player controls popoutBounds: null | DOMRect; // bounding box of current popout }; From d6def996bf1b4384de81b351ef3e879b464224e2 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 20 Apr 2023 21:26:37 +0200 Subject: [PATCH 22/23] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 097149c6..2223761f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movie-web", - "version": "3.0.10", + "version": "3.0.11", "private": true, "homepage": "https://movie.squeezebox.dev", "dependencies": { From 2cfd7e64a2c3d06a19dfdc8bc24b02535c10c59b Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 20 Apr 2023 21:29:47 +0200 Subject: [PATCH 23/23] remove gdrive from bundle --- src/backend/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/index.ts b/src/backend/index.ts index 5cf50906..3d5a33c1 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -1,7 +1,7 @@ import { initializeScraperStore } from "./helpers/register"; // providers -import "./providers/gdriveplayer"; +// import "./providers/gdriveplayer"; import "./providers/flixhq"; import "./providers/superstream"; import "./providers/netfilm";