diff --git a/.vscode/settings.json b/.vscode/settings.json
index e7ba0852..99e533d7 100755
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,5 +4,24 @@
"docify.inlineComments": true,
"docify.moreExpressiveComments": true,
"docify.sidePanelReviewMode": false,
- "docify.programmingLanguage": "javascript"
+ "docify.programmingLanguage": "javascript",
+ "workbench.colorCustomizations": {
+ "activityBar.activeBackground": "#ff9396",
+ "activityBar.background": "#ff9396",
+ "activityBar.foreground": "#15202b",
+ "activityBar.inactiveForeground": "#15202b99",
+ "activityBarBadge.background": "#048000",
+ "activityBarBadge.foreground": "#e7e7e7",
+ "commandCenter.border": "#15202b99",
+ "sash.hoverBorder": "#ff9396",
+ "statusBar.background": "#ff6064",
+ "statusBar.foreground": "#15202b",
+ "statusBarItem.hoverBackground": "#ff2d32",
+ "statusBarItem.remoteBackground": "#ff6064",
+ "statusBarItem.remoteForeground": "#15202b",
+ "titleBar.activeBackground": "#ff6064",
+ "titleBar.activeForeground": "#15202b",
+ "titleBar.inactiveBackground": "#ff606499",
+ "titleBar.inactiveForeground": "#15202b99"
+ }
}
\ No newline at end of file
diff --git a/comty.js b/comty.js
index 126bad9c..15f89af3 160000
--- a/comty.js
+++ b/comty.js
@@ -1 +1 @@
-Subproject commit 126bad9c1e21c0c7fcab60a22fc9d70bbbd9a999
+Subproject commit 15f89af37c5bac086e8e1154f5d1b3da8967678b
diff --git a/linebridge b/linebridge
index 6d553830..d2e6f1bc 160000
--- a/linebridge
+++ b/linebridge
@@ -1 +1 @@
-Subproject commit 6d553830ab4661ffab952253d77ccb0bfc1363d8
+Subproject commit d2e6f1bc5856e3084d4fd068dec5d67ab2ef9d8d
diff --git a/packages/app/config/defaultSettings.json b/packages/app/config/defaultSettings.json
index b943653b..e99c7641 100755
--- a/packages/app/config/defaultSettings.json
+++ b/packages/app/config/defaultSettings.json
@@ -1,5 +1,5 @@
{
- "app:language": "en",
+ "app:language": "en_US",
"low_performance_mode": false,
"transcode_video_browser": false,
"forceMobileMode": false,
diff --git a/packages/app/config/index.js b/packages/app/config/index.js
index fb02777f..2bdb9cce 100755
--- a/packages/app/config/index.js
+++ b/packages/app/config/index.js
@@ -56,14 +56,14 @@ export default {
i18n: {
languages: [
{
- locale: "en",
+ locale: "en_US",
name: "English"
},
{
- locale: "es",
+ locale: "es_ES",
name: "Español"
}
],
- defaultLocale: "en",
+ defaultLocale: "en_US",
}
}
\ No newline at end of file
diff --git a/packages/app/config/languages.json b/packages/app/config/languages.json
new file mode 100644
index 00000000..2074958e
--- /dev/null
+++ b/packages/app/config/languages.json
@@ -0,0 +1,611 @@
+{
+ "ab": "Abkhazian",
+ "ace": "Achinese",
+ "ach": "Acoli",
+ "ada": "Adangme",
+ "ady": "Adyghe",
+ "aa": "Afar",
+ "afh": "Afrihili",
+ "af": "Afrikaans",
+ "agq": "Aghem",
+ "ain": "Ainu",
+ "ak": "Akan",
+ "akk": "Akkadian",
+ "bss": "Akoose",
+ "akz": "Alabama",
+ "sq": "Albanian",
+ "ale": "Aleut",
+ "arq": "Algerian Arabic",
+ "am": "Amarik",
+ "en_US": "American English",
+ "ase": "American Sign Language",
+ "egy": "Ancient Egyptian",
+ "grc": "Ancient Greek",
+ "anp": "Angika",
+ "njo": "Ao Naga",
+ "ar": "Arabik",
+ "an": "Aragonese",
+ "arc": "Aramaic",
+ "aro": "Araona",
+ "arp": "Arapaho",
+ "arw": "Arawak",
+ "hy": "Armenian",
+ "rup": "Aromanian",
+ "frp": "Arpitan",
+ "as": "Assamese",
+ "ast": "Asturian",
+ "asa": "Asu",
+ "cch": "Atsam",
+ "en_AU": "Australian English",
+ "de_AT": "Austrian German",
+ "av": "Avaric",
+ "ae": "Avestan",
+ "awa": "Awadhi",
+ "ay": "Aymara",
+ "az": "Azerbaijani",
+ "bfq": "Badaga",
+ "ksf": "Bafia",
+ "bfd": "Bafut",
+ "bqi": "Bakhtiari",
+ "ban": "Balinese",
+ "bal": "Baluchi",
+ "bm": "Bambara",
+ "bax": "Bamun",
+ "bjn": "Banjar",
+ "bas": "Basaa",
+ "ba": "Bashkir",
+ "eu": "Basque",
+ "bbc": "Batak Toba",
+ "bar": "Bavarian",
+ "bej": "Beja",
+ "be": "Belarus kasa",
+ "bem": "Bemba",
+ "bez": "Bena",
+ "bn": "Bengali kasa",
+ "bew": "Betawi",
+ "my": "B\u025b\u025bmis kasa",
+ "bho": "Bhojpuri",
+ "bik": "Bikol",
+ "bin": "Bini",
+ "bpy": "Bishnupriya",
+ "bi": "Bislama",
+ "byn": "Blin",
+ "zbl": "Blissymbols",
+ "brx": "Bodo",
+ "en": "Bor\u0254fo",
+ "bs": "Bosnian",
+ "bg": "B\u0254lgeria kasa",
+ "brh": "Brahui",
+ "bra": "Braj",
+ "pt_BR": "Brazilian Portuguese",
+ "br": "Breton",
+ "en_GB": "British English",
+ "bug": "Buginese",
+ "bum": "Bulu",
+ "bua": "Buriat",
+ "cad": "Caddo",
+ "frc": "Cajun French",
+ "en_CA": "Canadian English",
+ "fr_CA": "Canadian French",
+ "yue": "Cantonese",
+ "cps": "Capiznon",
+ "car": "Carib",
+ "ca": "Catalan",
+ "cay": "Cayuga",
+ "ceb": "Cebuano",
+ "tzm": "Central Atlas Tamazight",
+ "dtp": "Central Dusun",
+ "ckb": "Central Kurdish",
+ "esu": "Central Yupik",
+ "shu": "Chadian Arabic",
+ "chg": "Chagatai",
+ "ch": "Chamorro",
+ "ce": "Chechen",
+ "chr": "Cherokee",
+ "chy": "Cheyenne",
+ "chb": "Chibcha",
+ "cgg": "Chiga",
+ "qug": "Chimborazo Highland Quichua",
+ "chn": "Chinook Jargon",
+ "chp": "Chipewyan",
+ "cho": "Choctaw",
+ "cu": "Church Slavic",
+ "chk": "Chuukese",
+ "cv": "Chuvash",
+ "nwc": "Classical Newari",
+ "syc": "Classical Syriac",
+ "ksh": "Colognian",
+ "swb": "Comorian",
+ "swc": "Congo Swahili",
+ "cop": "Coptic",
+ "kw": "Cornish",
+ "co": "Corsican",
+ "cr": "Cree",
+ "mus": "Creek",
+ "crh": "Crimean Turkish",
+ "hr": "Croatian",
+ "dak": "Dakota",
+ "da": "Danish",
+ "dar": "Dargwa",
+ "dzg": "Dazaga",
+ "del": "Delaware",
+ "nl": "D\u025b\u025bkye",
+ "din": "Dinka",
+ "dv": "Divehi",
+ "doi": "Dogri",
+ "dgr": "Dogrib",
+ "dua": "Duala",
+ "dyu": "Dyula",
+ "dz": "Dzongkha",
+ "frs": "Eastern Frisian",
+ "efi": "Efik",
+ "arz": "Egyptian Arabic",
+ "eka": "Ekajuk",
+ "elx": "Elamite",
+ "ebu": "Embu",
+ "egl": "Emilian",
+ "myv": "Erzya",
+ "eo": "Esperanto",
+ "et": "Estonian",
+ "pt_PT": "Portuguese",
+ "es_ES": "Español",
+ "ee": "Ewe",
+ "ewo": "Ewondo",
+ "ext": "Extremaduran",
+ "fan": "Fang",
+ "fat": "Fanti",
+ "fo": "Faroese",
+ "hif": "Fiji Hindi",
+ "fj": "Fijian",
+ "fil": "Filipino",
+ "fi": "Finnish",
+ "nl_BE": "Flemish",
+ "fon": "Fon",
+ "gur": "Frafra",
+ "fr": "Fr\u025bnkye",
+ "fur": "Friulian",
+ "ff": "Fulah",
+ "gaa": "Ga",
+ "gag": "Gagauz",
+ "gl": "Galician",
+ "gan": "Gan Chinese",
+ "lg": "Ganda",
+ "gay": "Gayo",
+ "gba": "Gbaya",
+ "gez": "Geez",
+ "ka": "Georgian",
+ "aln": "Gheg Albanian",
+ "bbj": "Ghomala",
+ "glk": "Gilaki",
+ "gil": "Gilbertese",
+ "gom": "Goan Konkani",
+ "gon": "Gondi",
+ "gor": "Gorontalo",
+ "got": "Gothic",
+ "grb": "Grebo",
+ "el": "Greek kasa",
+ "gn": "Guarani",
+ "gu": "Gujarati",
+ "guz": "Gusii",
+ "gwi": "Gwich\u02bcin",
+ "de": "Gyaaman",
+ "jv": "Gyabanis kasa",
+ "ja": "Gyapan kasa",
+ "hai": "Haida",
+ "ht": "Haitian",
+ "hak": "Hakka Chinese",
+ "hu": "Hangri kasa",
+ "ha": "Hausa",
+ "haw": "Hawaiian",
+ "he": "Hebrew",
+ "hz": "Herero",
+ "hil": "Hiligaynon",
+ "hi": "Hindi",
+ "ho": "Hiri Motu",
+ "hit": "Hittite",
+ "hmn": "Hmong",
+ "hup": "Hupa",
+ "iba": "Iban",
+ "ibb": "Ibibio",
+ "is": "Icelandic",
+ "io": "Ido",
+ "ig": "Igbo",
+ "ilo": "Iloko",
+ "smn": "Inari Sami",
+ "id": "Indonihyia kasa",
+ "izh": "Ingrian",
+ "inh": "Ingush",
+ "ia": "Interlingua",
+ "ie": "Interlingue",
+ "iu": "Inuktitut",
+ "ik": "Inupiaq",
+ "ga": "Irish",
+ "it": "Italy kasa",
+ "jam": "Jamaican Creole English",
+ "kaj": "Jju",
+ "dyo": "Jola-Fonyi",
+ "jrb": "Judeo-Arabic",
+ "jpr": "Judeo-Persian",
+ "jut": "Jutish",
+ "kbd": "Kabardian",
+ "kea": "Kabuverdianu",
+ "kab": "Kabyle",
+ "kac": "Kachin",
+ "kgp": "Kaingang",
+ "kkj": "Kako",
+ "kl": "Kalaallisut",
+ "kln": "Kalenjin",
+ "xal": "Kalmyk",
+ "kam": "Kamba",
+ "km": "Kambodia kasa",
+ "kbl": "Kanembu",
+ "kn": "Kannada",
+ "kr": "Kanuri",
+ "kaa": "Kara-Kalpak",
+ "krc": "Karachay-Balkar",
+ "krl": "Karelian",
+ "ks": "Kashmiri",
+ "csb": "Kashubian",
+ "kaw": "Kawi",
+ "kk": "Kazakh",
+ "ken": "Kenyang",
+ "kha": "Khasi",
+ "kho": "Khotanese",
+ "khw": "Khowar",
+ "ki": "Kikuyu",
+ "kmb": "Kimbundu",
+ "krj": "Kinaray-a",
+ "kiu": "Kirmanjki",
+ "tlh": "Klingon",
+ "bkm": "Kom",
+ "kv": "Komi",
+ "koi": "Komi-Permyak",
+ "kg": "Kongo",
+ "kok": "Konkani",
+ "ko": "Korea kasa",
+ "kfo": "Koro",
+ "kos": "Kosraean",
+ "avk": "Kotava",
+ "khq": "Koyra Chiini",
+ "ses": "Koyraboro Senni",
+ "kpe": "Kpelle",
+ "kri": "Krio",
+ "kj": "Kuanyama",
+ "kum": "Kumyk",
+ "ku": "Kurdish",
+ "kru": "Kurukh",
+ "kut": "Kutenai",
+ "nmg": "Kwasio",
+ "zh": "Kyaena kasa",
+ "cs": "Ky\u025bk kasa",
+ "ky": "Kyrgyz",
+ "quc": "K\u02bciche\u02bc",
+ "lad": "Ladino",
+ "lah": "Lahnda",
+ "lkt": "Lakota",
+ "lam": "Lamba",
+ "lag": "Langi",
+ "lo": "Lao",
+ "ltg": "Latgalian",
+ "la": "Latin",
+ "es_419": "Latin American Spanish",
+ "lv": "Latvian",
+ "lzz": "Laz",
+ "lez": "Lezghian",
+ "lij": "Ligurian",
+ "li": "Limburgish",
+ "ln": "Lingala",
+ "lfn": "Lingua Franca Nova",
+ "lzh": "Literary Chinese",
+ "lt": "Lithuanian",
+ "liv": "Livonian",
+ "jbo": "Lojban",
+ "lmo": "Lombard",
+ "nds": "Low German",
+ "sli": "Lower Silesian",
+ "dsb": "Lower Sorbian",
+ "loz": "Lozi",
+ "lu": "Luba-Katanga",
+ "lua": "Luba-Lulua",
+ "lui": "Luiseno",
+ "smj": "Lule Sami",
+ "lun": "Lunda",
+ "luo": "Luo",
+ "lb": "Luxembourgish",
+ "luy": "Luyia",
+ "mde": "Maba",
+ "mk": "Macedonian",
+ "jmc": "Machame",
+ "mad": "Madurese",
+ "maf": "Mafa",
+ "mag": "Magahi",
+ "vmf": "Main-Franconian",
+ "mai": "Maithili",
+ "mak": "Makasar",
+ "mgh": "Makhuwa-Meetto",
+ "kde": "Makonde",
+ "mg": "Malagasy",
+ "ms": "Malay kasa",
+ "ml": "Malayalam",
+ "mt": "Maltese",
+ "mnc": "Manchu",
+ "mdr": "Mandar",
+ "man": "Mandingo",
+ "mni": "Manipuri",
+ "gv": "Manx",
+ "mi": "Maori",
+ "arn": "Mapuche",
+ "mr": "Marathi",
+ "chm": "Mari",
+ "mh": "Marshallese",
+ "mwr": "Marwari",
+ "mas": "Masai",
+ "mzn": "Mazanderani",
+ "byv": "Medumba",
+ "men": "Mende",
+ "mwv": "Mentawai",
+ "mer": "Meru",
+ "mgo": "Meta\u02bc",
+ "es_MX": "Mexican Spanish",
+ "mic": "Micmac",
+ "dum": "Middle Dutch",
+ "enm": "Middle English",
+ "frm": "Middle French",
+ "gmh": "Middle High German",
+ "mga": "Middle Irish",
+ "nan": "Min Nan Chinese",
+ "min": "Minangkabau",
+ "xmf": "Mingrelian",
+ "mwl": "Mirandese",
+ "lus": "Mizo",
+ "ar_001": "Modern Standard Arabic",
+ "moh": "Mohawk",
+ "mdf": "Moksha",
+ "ro_MD": "Moldavian",
+ "lol": "Mongo",
+ "mn": "Mongolian",
+ "mfe": "Morisyen",
+ "ary": "Moroccan Arabic",
+ "mos": "Mossi",
+ "mul": "Multiple Languages",
+ "mua": "Mundang",
+ "ttt": "Muslim Tat",
+ "mye": "Myene",
+ "naq": "Nama",
+ "na": "Nauru",
+ "nv": "Navajo",
+ "ng": "Ndonga",
+ "nap": "Neapolitan",
+ "new": "Newari",
+ "ne": "N\u025bpal kasa",
+ "sba": "Ngambay",
+ "nnh": "Ngiemboon",
+ "jgo": "Ngomba",
+ "yrl": "Nheengatu",
+ "nia": "Nias",
+ "niu": "Niuean",
+ "zxx": "No linguistic content",
+ "nog": "Nogai",
+ "nd": "North Ndebele",
+ "frr": "Northern Frisian",
+ "se": "Northern Sami",
+ "nso": "Northern Sotho",
+ "no": "Norwegian",
+ "nb": "Norwegian Bokm\u00e5l",
+ "nn": "Norwegian Nynorsk",
+ "nov": "Novial",
+ "nus": "Nuer",
+ "nym": "Nyamwezi",
+ "ny": "Nyanja",
+ "nyn": "Nyankole",
+ "tog": "Nyasa Tonga",
+ "nyo": "Nyoro",
+ "nzi": "Nzima",
+ "nqo": "N\u02bcKo",
+ "oc": "Occitan",
+ "oj": "Ojibwa",
+ "ang": "Old English",
+ "fro": "Old French",
+ "goh": "Old High German",
+ "sga": "Old Irish",
+ "non": "Old Norse",
+ "peo": "Old Persian",
+ "pro": "Old Proven\u00e7al",
+ "or": "Oriya",
+ "om": "Oromo",
+ "osa": "Osage",
+ "os": "Ossetic",
+ "ota": "Ottoman Turkish",
+ "pal": "Pahlavi",
+ "pfl": "Palatine German",
+ "pau": "Palauan",
+ "pi": "Pali",
+ "pam": "Pampanga",
+ "pag": "Pangasinan",
+ "pap": "Papiamento",
+ "ps": "Pashto",
+ "pdc": "Pennsylvania German",
+ "fa": "P\u025b\u025bhyia kasa",
+ "phn": "Phoenician",
+ "pcd": "Picard",
+ "pms": "Piedmontese",
+ "pdt": "Plautdietsch",
+ "pon": "Pohnpeian",
+ "pnt": "Pontic",
+ "pl": "P\u0254land kasa",
+ "pt": "P\u0254\u0254tugal kasa",
+ "prg": "Prussian",
+ "pa": "Pungyabi kasa",
+ "qu": "Quechua",
+ "ru": "Rahyia kasa",
+ "raj": "Rajasthani",
+ "rap": "Rapanui",
+ "rar": "Rarotongan",
+ "rw": "Rewanda kasa",
+ "rif": "Riffian",
+ "rgn": "Romagnol",
+ "rm": "Romansh",
+ "rom": "Romany",
+ "rof": "Rombo",
+ "ro": "Romenia kasa",
+ "root": "Root",
+ "rtm": "Rotuman",
+ "rug": "Roviana",
+ "rn": "Rundi",
+ "rue": "Rusyn",
+ "rwk": "Rwa",
+ "ssy": "Saho",
+ "sah": "Sakha",
+ "sam": "Samaritan Aramaic",
+ "saq": "Samburu",
+ "sm": "Samoan",
+ "sgs": "Samogitian",
+ "sad": "Sandawe",
+ "sg": "Sango",
+ "sbp": "Sangu",
+ "sa": "Sanskrit",
+ "sat": "Santali",
+ "sc": "Sardinian",
+ "sas": "Sasak",
+ "sdc": "Sassarese Sardinian",
+ "stq": "Saterland Frisian",
+ "saz": "Saurashtra",
+ "sco": "Scots",
+ "gd": "Scottish Gaelic",
+ "sly": "Selayar",
+ "sel": "Selkup",
+ "seh": "Sena",
+ "see": "Seneca",
+ "sr": "Serbian",
+ "sh": "Serbo-Croatian",
+ "srr": "Serer",
+ "sei": "Seri",
+ "ksb": "Shambala",
+ "shn": "Shan",
+ "sn": "Shona",
+ "ii": "Sichuan Yi",
+ "scn": "Sicilian",
+ "sid": "Sidamo",
+ "bla": "Siksika",
+ "szl": "Silesian",
+ "zh_Hans": "Simplified Chinese",
+ "sd": "Sindhi",
+ "si": "Sinhala",
+ "sms": "Skolt Sami",
+ "den": "Slave",
+ "sk": "Slovak",
+ "sl": "Slovenian",
+ "xog": "Soga",
+ "sog": "Sogdien",
+ "so": "Somalia kasa",
+ "snk": "Soninke",
+ "azb": "South Azerbaijani",
+ "nr": "South Ndebele",
+ "alt": "Southern Altai",
+ "sma": "Southern Sami",
+ "st": "Southern Sotho",
+ "es": "Spain kasa",
+ "srn": "Sranan Tongo",
+ "zgh": "Standard Moroccan Tamazight",
+ "suk": "Sukuma",
+ "sux": "Sumerian",
+ "su": "Sundanese",
+ "sus": "Susu",
+ "sw": "Swahili",
+ "ss": "Swati",
+ "sv": "Sweden kasa",
+ "fr_CH": "Swiss French",
+ "gsw": "Swiss German",
+ "de_CH": "Swiss High German",
+ "syr": "Syriac",
+ "shi": "Tachelhit",
+ "th": "Taeland kasa",
+ "tl": "Tagalog",
+ "ty": "Tahitian",
+ "dav": "Taita",
+ "tg": "Tajik",
+ "tly": "Talysh",
+ "tmh": "Tamashek",
+ "ta": "Tamil kasa",
+ "trv": "Taroko",
+ "twq": "Tasawaq",
+ "tt": "Tatar",
+ "te": "Telugu",
+ "ter": "Tereno",
+ "teo": "Teso",
+ "tet": "Tetum",
+ "tr": "T\u025b\u025bki kasa",
+ "bo": "Tibetan",
+ "tig": "Tigre",
+ "ti": "Tigrinya",
+ "tem": "Timne",
+ "tiv": "Tiv",
+ "tli": "Tlingit",
+ "tpi": "Tok Pisin",
+ "tkl": "Tokelau",
+ "to": "Tongan",
+ "fit": "Tornedalen Finnish",
+ "zh_Hant": "Traditional Chinese",
+ "tkr": "Tsakhur",
+ "tsd": "Tsakonian",
+ "tsi": "Tsimshian",
+ "ts": "Tsonga",
+ "tn": "Tswana",
+ "tcy": "Tulu",
+ "tum": "Tumbuka",
+ "aeb": "Tunisian Arabic",
+ "tk": "Turkmen",
+ "tru": "Turoyo",
+ "tvl": "Tuvalu",
+ "tyv": "Tuvinian",
+ "tw": "Twi",
+ "kcg": "Tyap",
+ "udm": "Udmurt",
+ "uga": "Ugaritic",
+ "uk": "Ukren kasa",
+ "umb": "Umbundu",
+ "und": "Unknown Language",
+ "hsb": "Upper Sorbian",
+ "ur": "Urdu kasa",
+ "ug": "Uyghur",
+ "uz": "Uzbek",
+ "vai": "Vai",
+ "ve": "Venda",
+ "vec": "Venetian",
+ "vep": "Veps",
+ "vi": "Vi\u025btnam kasa",
+ "vo": "Volap\u00fck",
+ "vro": "V\u00f5ro",
+ "vot": "Votic",
+ "vun": "Vunjo",
+ "wa": "Walloon",
+ "wae": "Walser",
+ "war": "Waray",
+ "wbp": "Warlpiri",
+ "was": "Washo",
+ "guc": "Wayuu",
+ "cy": "Welsh",
+ "vls": "West Flemish",
+ "fy": "Western Frisian",
+ "mrj": "Western Mari",
+ "wal": "Wolaytta",
+ "wo": "Wolof",
+ "wuu": "Wu Chinese",
+ "xh": "Xhosa",
+ "hsn": "Xiang Chinese",
+ "yav": "Yangben",
+ "yao": "Yao",
+ "yap": "Yapese",
+ "ybb": "Yemba",
+ "yi": "Yiddish",
+ "yo": "Yoruba",
+ "zap": "Zapotec",
+ "dje": "Zarma",
+ "zza": "Zaza",
+ "zea": "Zeelandic",
+ "zen": "Zenaga",
+ "za": "Zhuang",
+ "gbz": "Zoroastrian Dari",
+ "zu": "Zulu",
+ "zun": "Zuni"
+}
\ No newline at end of file
diff --git a/packages/app/config/sidebar.json b/packages/app/config/sidebar.json
index 1767a04a..435ef2b6 100755
--- a/packages/app/config/sidebar.json
+++ b/packages/app/config/sidebar.json
@@ -1,9 +1,15 @@
[
+ {
+ "id": "home",
+ "path": "/",
+ "title": "Home",
+ "icon": "Home"
+ },
{
"id": "timeline",
"path": "/",
"title": "Timeline",
- "icon": "Home"
+ "icon": "MdTag"
},
{
"id": "tv",
@@ -16,18 +22,5 @@
"path": "/music",
"title": "Music",
"icon": "MdMusicNote"
- },
- {
- "id": "groups",
- "path": "/groups",
- "title": "Groups",
- "icon": "MdGroups",
- "disabled": true
- },
- {
- "id": "Marketplace",
- "path": "/marketplace",
- "title": "Marketplace",
- "icon": "Box"
}
]
\ No newline at end of file
diff --git a/packages/app/config/translations/en.json b/packages/app/config/translations/en_US.json
similarity index 100%
rename from packages/app/config/translations/en.json
rename to packages/app/config/translations/en_US.json
diff --git a/packages/app/config/translations/es.json b/packages/app/config/translations/es_ES.json
similarity index 100%
rename from packages/app/config/translations/es.json
rename to packages/app/config/translations/es_ES.json
diff --git a/packages/app/package.json b/packages/app/package.json
index b904d703..85255a9d 100755
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -6,13 +6,9 @@
"author": "RageStudio",
"description": "A prototype of a social network.",
"scripts": {
- "start": "electron-forge start",
"build": "vite build",
"dev": "vite",
- "dev:electron": "concurrently -k \"yarn dev\" \"yarn electron:dev\"",
- "electron:dev": "cross-env IS_DEV=true electron-forge start",
"docker-compose:update_run": "docker-compose down && git pull && yarn build && docker-compose up -d --build",
- "electron:build": "cross-env IS_DEV=false electron-builder -mwl --dir",
"preview": "vite preview"
},
"peerDependencies": {
diff --git a/packages/app/src-tauri/tauri.conf.json b/packages/app/src-tauri/tauri.conf.json
index 031150e6..8da62c65 100644
--- a/packages/app/src-tauri/tauri.conf.json
+++ b/packages/app/src-tauri/tauri.conf.json
@@ -1,6 +1,6 @@
{
"build": {
- "devPath": "http://fr01.ragestudio.net:8000",
+ "devPath": "https://fr01.ragestudio.net:8000",
"distDir": "../dist"
},
"package": {
diff --git a/packages/app/src/App.jsx b/packages/app/src/App.jsx
index e8bca0a1..214683da 100755
--- a/packages/app/src/App.jsx
+++ b/packages/app/src/App.jsx
@@ -106,7 +106,7 @@ class ComtyApp extends React.Component {
}
},
openLoginForm: async (options = {}) => {
- app.DrawerController.open("login", Login, {
+ app.layout.drawer.open("login", Login, {
defaultLocked: options.defaultLocked ?? false,
componentProps: {
sessionController: this.sessionController,
@@ -120,7 +120,7 @@ class ComtyApp extends React.Component {
})
},
openRegisterForm: async (options = {}) => {
- app.DrawerController.open("Register", UserRegister, {
+ app.layout.drawer.open("Register", UserRegister, {
defaultLocked: options.defaultLocked ?? false,
componentProps: {
sessionController: this.sessionController,
@@ -144,7 +144,7 @@ class ComtyApp extends React.Component {
},
openSearcher: (options) => {
if (app.isMobile) {
- return app.DrawerController.open("searcher", Searcher, {
+ return app.layout.drawer.open("searcher", Searcher, {
...options,
componentProps: {
renderResults: true,
diff --git a/packages/app/src/components/BackgroundDecorator/index.jsx b/packages/app/src/components/BackgroundDecorator/index.jsx
index e126654b..4d568013 100755
--- a/packages/app/src/components/BackgroundDecorator/index.jsx
+++ b/packages/app/src/components/BackgroundDecorator/index.jsx
@@ -23,7 +23,7 @@ export default () => {
React.useEffect(() => {
app.eventBus.on("style.update", handleStyleUpdate)
- const activeSVG = app.cores.style.getValue("backgroundSVG")
+ const activeSVG = app.cores.style.getVar("backgroundSVG")
if (hasBackgroundSVG(activeSVG)) {
setActiveColor(true)
diff --git a/packages/app/src/components/CoverEditor/index.jsx b/packages/app/src/components/CoverEditor/index.jsx
new file mode 100644
index 00000000..eef48241
--- /dev/null
+++ b/packages/app/src/components/CoverEditor/index.jsx
@@ -0,0 +1,58 @@
+import React from "react"
+import * as antd from "antd"
+
+import Image from "@components/Image"
+import UploadButton from "@components/UploadButton"
+
+import "./index.less"
+
+const CoverEditor = (props) => {
+ const { value, onChange, defaultUrl } = props
+
+ const [url, setUrl] = React.useState(value)
+
+ React.useEffect(() => {
+ setUrl(value)
+ }, [value])
+
+ React.useEffect(() => {
+ onChange(url)
+ }, [url])
+
+ React.useEffect(() => {
+ if (!url) {
+ setUrl(defaultUrl)
+ }
+ }, [])
+
+ return
+
+
+
+
+
+
{
+ setUrl(response.url)
+ }}
+ />
+
+ {
+ setUrl(defaultUrl)
+ }}
+ >
+ Reset
+
+
+ {
+ props.extraActions
+ }
+
+
+}
+
+export default CoverEditor
diff --git a/packages/app/src/components/CoverEditor/index.less b/packages/app/src/components/CoverEditor/index.less
new file mode 100644
index 00000000..6d4dd835
--- /dev/null
+++ b/packages/app/src/components/CoverEditor/index.less
@@ -0,0 +1,45 @@
+.cover-editor {
+ display: flex;
+
+ flex-direction: column;
+ width: 100%;
+
+ gap: 10px;
+
+ .cover-editor-preview {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+ justify-content: center;
+
+ width: 100%;
+
+ padding: 7px;
+
+ background-color: var(--background-color-accent);
+
+ border-radius: 12px;
+
+ .lazy-load-image-background {
+ max-width: 200px;
+ max-height: 200px;
+
+ img {
+ height: 100%;
+ width: 100%;
+
+ border-radius: 12px;
+
+ object-fit: contain;
+ }
+ }
+ }
+
+ .cover-editor-actions {
+ display: flex;
+ flex-direction: row;
+
+ gap: 20px;
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/Login/index.jsx b/packages/app/src/components/Login/index.jsx
index 2e992718..a46436d8 100755
--- a/packages/app/src/components/Login/index.jsx
+++ b/packages/app/src/components/Login/index.jsx
@@ -113,20 +113,6 @@ class Login extends React.Component {
app.location.push("/apr")
}
- onClickRegister = () => {
- if (this.props.locked) {
- this.props.unlock()
- }
-
- if (typeof this.props.close === "function") {
- this.props.close()
- }
-
- app.controls.openRegisterForm({
- defaultLocked: this.props.locked
- })
- }
-
toggleLoading = (to) => {
if (typeof to === "undefined") {
to = !this.state.loading
@@ -351,10 +337,6 @@ class Login extends React.Component {
-
-
}
diff --git a/packages/app/src/components/Music/PlaylistView/index.jsx b/packages/app/src/components/Music/PlaylistView/index.jsx
index 4ef1530b..7712a136 100755
--- a/packages/app/src/components/Music/PlaylistView/index.jsx
+++ b/packages/app/src/components/Music/PlaylistView/index.jsx
@@ -5,11 +5,12 @@ import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
import fuse from "fuse.js"
-import useWsEvents from "@hooks/useWsEvents"
-
import { WithPlayerContext } from "@contexts/WithPlayerContext"
import { Context as PlaylistContext } from "@contexts/WithPlaylistContext"
+import useWsEvents from "@hooks/useWsEvents"
+import checkUserIdIsSelf from "@utils/checkUserIdIsSelf"
+
import LoadMore from "@components/LoadMore"
import { Icons } from "@components/Icons"
import MusicTrack from "@components/Music/Track"
@@ -75,7 +76,7 @@ const MoreMenuHandlers = {
export default (props) => {
const [playlist, setPlaylist] = React.useState(props.playlist)
const [searchResults, setSearchResults] = React.useState(null)
- const [owningPlaylist, setOwningPlaylist] = React.useState(app.cores.permissions.checkUserIdIsSelf(props.playlist?.user_id))
+ const [owningPlaylist, setOwningPlaylist] = React.useState(checkUserIdIsSelf(props.playlist?.user_id))
const moreMenuItems = React.useMemo(() => {
const items = [{
@@ -84,7 +85,7 @@ export default (props) => {
}]
if (!playlist.type || playlist.type === "playlist") {
- if (app.cores.permissions.checkUserIdIsSelf(playlist.user_id)) {
+ if (checkUserIdIsSelf(playlist.user_id)) {
items.push({
key: "delete",
label: "Delete",
@@ -221,7 +222,7 @@ export default (props) => {
React.useEffect(() => {
setPlaylist(props.playlist)
- setOwningPlaylist(app.cores.permissions.checkUserIdIsSelf(props.playlist?.user_id))
+ setOwningPlaylist(checkUserIdIsSelf(props.playlist?.user_id))
}, [props.playlist])
if (!playlist) {
diff --git a/packages/app/src/components/MusicStudio/LyricsEditor/index.jsx b/packages/app/src/components/MusicStudio/LyricsEditor/index.jsx
new file mode 100644
index 00000000..e46bbd81
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/LyricsEditor/index.jsx
@@ -0,0 +1,116 @@
+import React from "react"
+import * as antd from "antd"
+
+import LyricsTextView from "../LyricsTextView"
+import UploadButton from "@components/UploadButton"
+import { Icons } from "@components/Icons"
+
+import MusicService from "@models/music"
+
+import Languages from "@config/languages"
+
+const LanguagesMap = Object.entries(Languages).map(([key, value]) => {
+ return {
+ label: value,
+ value: key,
+ }
+})
+
+import "./index.less"
+
+const LyricsEditor = (props) => {
+ const [L_TrackLyrics, R_TrackLyrics, E_TrackLyrics, F_TrackLyrics] = app.cores.api.useRequest(MusicService.getTrackLyrics, props.track._id)
+
+ const [langs, setLangs] = React.useState([])
+ const [selectedLang, setSelectedLang] = React.useState("original")
+
+ async function onUploadLRC(uid, data) {
+ const { url } = data
+
+ setLangs((prev) => {
+ const index = prev.findIndex((lang) => {
+ return lang.id === selectedLang
+ })
+
+ console.log(`Replacing value for id [${selectedLang}] at index [${index}]`)
+
+ if (index !== -1) {
+ prev[index].value = url
+ } else {
+ const lang = LanguagesMap.find((lang) => {
+ return lang.value === selectedLang
+ })
+
+ prev.push({
+ id: lang.value,
+ name: lang.label,
+ value: url
+ })
+ }
+
+ console.log(`new value =>`, prev)
+
+ return prev
+ })
+ }
+
+ React.useEffect(() => {
+ if (R_TrackLyrics) {
+ if (R_TrackLyrics.available_langs) {
+ setLangs(R_TrackLyrics.available_langs)
+ }
+ }
+ console.log(R_TrackLyrics)
+ }, [R_TrackLyrics])
+
+ const currentLangData = selectedLang && langs.find((lang) => {
+ return lang.id === selectedLang
+ })
+
+ console.log(langs, currentLangData)
+
+ return
+
Lyrics
+
+
(option?.label.toLowerCase() ?? '').includes(input.toLowerCase())}
+ filterSort={(optionA, optionB) =>
+ (optionA?.label.toLowerCase() ?? '').toLowerCase().localeCompare((optionB?.label.toLowerCase() ?? '').toLowerCase())
+ }
+ onChange={setSelectedLang}
+ />
+
+
+ {selectedLang}
+
+
+ {
+ selectedLang &&
+ }
+
+ {
+ currentLangData && currentLangData?.value &&
+ }
+ {
+ !currentLangData || !currentLangData?.value &&
+ }
+
+}
+
+export default LyricsEditor
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/LyricsEditor/index.less b/packages/app/src/components/MusicStudio/LyricsEditor/index.less
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/app/src/components/MusicStudio/LyricsTextView/index.jsx b/packages/app/src/components/MusicStudio/LyricsTextView/index.jsx
new file mode 100644
index 00000000..05f5f728
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/LyricsTextView/index.jsx
@@ -0,0 +1,60 @@
+import React from "react"
+import * as antd from "antd"
+import axios from "axios"
+
+const LyricsTextView = (props) => {
+ const { lang, track } = props
+
+ const [loading, setLoading] = React.useState(false)
+ const [error, setError] = React.useState(null)
+ const [lyrics, setLyrics] = React.useState(null)
+
+ async function getLyrics(resource_url) {
+ setError(null)
+ setLoading(true)
+ setLyrics(null)
+
+ const data = await axios({
+ method: "get",
+ url: resource_url,
+ responseType: "text"
+ }).catch((err) => {
+ console.error(err)
+ setError(err)
+
+ return null
+ })
+
+ if (data) {
+ setLyrics(data.data)
+ }
+
+ setLoading(false)
+ }
+
+ React.useEffect(() => {
+ getLyrics(lang.value)
+ }, [lang])
+
+ if (!lang) {
+ return null
+ }
+
+ if (error) {
+ return
+ }
+
+ if (loading) {
+ return
+ }
+
+ return
+}
+
+export default LyricsTextView
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/MyReleasesList/index.jsx b/packages/app/src/components/MusicStudio/MyReleasesList/index.jsx
new file mode 100644
index 00000000..973a515b
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/MyReleasesList/index.jsx
@@ -0,0 +1,55 @@
+import React from "react"
+import * as antd from "antd"
+
+import ReleaseItem from "@components/MusicStudio/ReleaseItem"
+
+import MusicModel from "@models/music"
+
+import "./index.less"
+
+const MyReleasesList = () => {
+ const [L_MyReleases, R_MyReleases, E_MyReleases, M_MyReleases] = app.cores.api.useRequest(MusicModel.getMyReleases, {
+ offset: 0,
+ limit: 100,
+ })
+
+ async function onClickReleaseItem(release) {
+ app.location.push(`/studio/music/${release._id}`)
+ }
+
+ return
+
+
Your Releases
+
+
+ {
+ L_MyReleases && !E_MyReleases &&
+ }
+ {
+ E_MyReleases &&
+ }
+ {
+ !L_MyReleases && !E_MyReleases && R_MyReleases && R_MyReleases.items.length === 0 &&
+ }
+
+ {
+ !L_MyReleases && !E_MyReleases && R_MyReleases && R_MyReleases.items.length > 0 &&
+ {
+ R_MyReleases.items.map((item) => {
+ return
+ })
+ }
+
+ }
+
+}
+
+export default MyReleasesList
\ No newline at end of file
diff --git a/packages/app/src/pages/music/dashboard/index.less b/packages/app/src/components/MusicStudio/MyReleasesList/index.less
similarity index 57%
rename from packages/app/src/pages/music/dashboard/index.less
rename to packages/app/src/components/MusicStudio/MyReleasesList/index.less
index 04a7f541..e6bf1d52 100644
--- a/packages/app/src/pages/music/dashboard/index.less
+++ b/packages/app/src/components/MusicStudio/MyReleasesList/index.less
@@ -1,8 +1,8 @@
-.music-dashboard {
+.music-studio-page-releases-list {
display: flex;
flex-direction: column;
width: 100%;
- .music-dashboard_header {}
+ gap: 10px;
}
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx b/packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx
new file mode 100644
index 00000000..ea5b1628
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx
@@ -0,0 +1,107 @@
+import React from "react"
+import * as antd from "antd"
+
+import { Icons } from "@components/Icons"
+
+import MusicModel from "@models/music"
+
+import Tabs from "./tabs"
+
+import "./index.less"
+
+const ReleaseEditor = (props) => {
+ const { release_id } = props
+
+ const basicInfoRef = React.useRef()
+
+ const [selectedTab, setSelectedTab] = React.useState("info")
+ const [L_Release, R_Release, E_Release, F_Release] = release_id !== "new" ? app.cores.api.useRequest(MusicModel.getReleaseData, release_id) : [false, false, false, false]
+
+ async function handleSubmit() {
+ basicInfoRef.current.submit()
+ }
+
+ async function onFinish(values) {
+ console.log(values)
+ }
+
+ async function canFinish() {
+ return true
+ }
+
+ if (E_Release) {
+ return
+ }
+
+ if (L_Release) {
+ return
+ }
+
+ const Tab = Tabs.find(({ key }) => key === selectedTab)
+
+ return
+
+
setSelectedTab(e.key)}
+ selectedKeys={[selectedTab]}
+ items={Tabs}
+ mode="vertical"
+ />
+
+
+
}
+ disabled={L_Release || !canFinish()}
+ >
+ Save
+
+
+ {
+ release_id !== "new" ?
}
+ disabled={L_Release}
+ >
+ Delete
+ : null
+ }
+
+ {
+ release_id !== "new" ?
}
+ onClick={() => app.location.push(`/music/release/${R_Release._id}`)}
+ >
+ Go to release
+ : null
+ }
+
+
+
+
+ {
+ !Tab &&
+ }
+ {
+ Tab && React.createElement(Tab.render, {
+ release: R_Release,
+ onFinish: onFinish,
+
+ references: {
+ basic: basicInfoRef
+ }
+ })
+ }
+
+
+}
+
+export default ReleaseEditor
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/ReleaseEditor/index.less b/packages/app/src/components/MusicStudio/ReleaseEditor/index.less
new file mode 100644
index 00000000..671d6882
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/ReleaseEditor/index.less
@@ -0,0 +1,93 @@
+.music-studio-release-editor {
+ display: flex;
+ flex-direction: row;
+
+ width: 100%;
+
+ padding: 20px;
+
+ gap: 20px;
+
+ .music-studio-release-editor-header {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+
+ gap: 20px;
+
+ .title {
+ font-size: 1.7rem;
+ font-family: "Space Grotesk", sans-serif;
+ }
+ }
+
+ .music-studio-release-editor-menu {
+ display: flex;
+ flex-direction: column;
+
+ gap: 20px;
+
+ align-items: center;
+
+ .ant-btn {
+ width: 100%;
+ }
+
+ .ant-menu {
+ background-color: var(--background-color-accent) !important;
+ border-radius: 12px;
+
+ padding: 8px;
+
+ gap: 5px;
+
+ .ant-menu-item {
+ padding: 5px 10px !important;
+ }
+
+ .ant-menu-item-selected {
+ background-color: var(--background-color-primary-2) !important;
+ }
+ }
+
+ .music-studio-release-editor-menu-actions {
+ display: flex;
+ flex-direction: column;
+
+ gap: 10px;
+
+ width: 100%;
+ }
+ }
+
+ .music-studio-release-editor-content {
+ display: flex;
+ flex-direction: column;
+
+ width: 100%;
+
+ .music-studio-release-editor-tab {
+ display: flex;
+ flex-direction: column;
+
+ gap: 10px;
+
+ h1 {
+ margin: 0;
+ }
+
+ .ant-form-item {
+ margin-bottom: 10px;
+ }
+
+ label {
+ height: fit-content;
+
+ span {
+ font-weight: 600;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Advanced/index.jsx b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Advanced/index.jsx
new file mode 100644
index 00000000..23d67faa
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Advanced/index.jsx
@@ -0,0 +1,9 @@
+import React from "react"
+
+const ReleaseAdvanced = (props) => {
+ return
+
Advanced
+
+}
+
+export default ReleaseAdvanced
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/BasicInformation/index.jsx b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/BasicInformation/index.jsx
new file mode 100644
index 00000000..af4205dd
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/BasicInformation/index.jsx
@@ -0,0 +1,105 @@
+import React from "react"
+import * as antd from "antd"
+
+import { Icons } from "@components/Icons"
+
+import CoverEditor from "@components/CoverEditor"
+
+const ReleasesTypes = [
+ {
+ value: "single",
+ label: "Single",
+ icon: ,
+ },
+ {
+ value: "ep",
+ label: "Episode",
+ icon: ,
+ },
+ {
+ value: "album",
+ label: "Album",
+ icon: ,
+ },
+ {
+ value: "compilation",
+ label: "Compilation",
+ icon: ,
+ }
+]
+
+const BasicInformation = (props) => {
+ const { release, onFinish } = props
+
+ return
+
Release Information
+
+
+
+
+
+
+ {
+ release._id && ID>}
+ name="_id"
+ initialValue={release._id}
+ disabled
+ >
+
+
+ }
+
+ Title>}
+ name="title"
+ rules={[{ required: true, message: "Input a title for the release" }]}
+ initialValue={release?.title}
+ >
+
+
+
+ Type>}
+ name="type"
+ rules={[{ required: true, message: "Select a type for the release" }]}
+ initialValue={release?.type}
+ >
+
+
+
+ Public>}
+ name="public"
+ initialValue={release?.public}
+ >
+
+
+
+
+}
+
+export default BasicInformation
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/index.jsx b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/index.jsx
new file mode 100644
index 00000000..2ccebcf0
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/index.jsx
@@ -0,0 +1,249 @@
+import React from "react"
+import * as antd from "antd"
+import classnames from "classnames"
+import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"
+
+import { Icons } from "@components/Icons"
+import TrackEditor from "@components/MusicStudio/TrackEditor"
+
+import "./index.less"
+
+const UploadHint = (props) => {
+ return
+
+
Upload your tracks
+
Drag and drop your tracks here or click this box to start uploading files.
+
+}
+
+const TrackListItem = (props) => {
+ const [loading, setLoading] = React.useState(false)
+ const [error, setError] = React.useState(null)
+
+ const { track } = props
+
+ async function onClickEditTrack() {
+ app.layout.drawer.open("track_editor", TrackEditor, {
+ type: "drawer",
+ props: {
+ width: "600px",
+ headerStyle: {
+ display: "none",
+ }
+ },
+ componentProps: {
+ track,
+ onSave: (newTrackData) => {
+ console.log("Saving track", newTrackData)
+ },
+ },
+ })
+ }
+
+ return
+ {
+ (provided, snapshot) => {
+ return
+
+ {props.index + 1}
+
+
+
{track.title}
+
+
+
}
+ onClick={onClickEditTrack}
+ />
+
+
+
+
+
+
+ }
+ }
+
+}
+
+const ReleaseTracks = (props) => {
+ const { release } = props
+
+ const [list, setList] = React.useState(release.list ?? [])
+ const [pendingTracksUpload, setPendingTracksUpload] = React.useState([])
+
+ async function onTrackUploaderChange (change) {
+ switch (change.file.status) {
+ case "uploading": {
+ if (!pendingTracksUpload.includes(change.file.uid)) {
+ pendingTracksUpload.push(change.file.uid)
+ }
+
+ setList((prev) => {
+ return [
+ ...prev,
+
+ ]
+ })
+
+ break
+ }
+ case "done": {
+ // remove pending file
+ this.setState({
+ pendingTracksUpload: this.state.pendingTracksUpload.filter((uid) => uid !== change.file.uid)
+ })
+
+ // update file url in the track info
+ const track = this.state.trackList.find((file) => file.uid === change.file.uid)
+
+ if (track) {
+ track.source = change.file.response.url
+ track.status = "done"
+ }
+
+ this.setState({
+ trackList: this.state.trackList
+ })
+
+ break
+ }
+ case "error": {
+ // remove pending file
+ this.handleTrackRemove(change.file.uid)
+
+ // open a dialog to show the error and ask user to retry
+ antd.Modal.error({
+ title: "Upload failed",
+ content: "An error occurred while uploading the file. You want to retry?",
+ cancelText: "No",
+ okText: "Retry",
+ onOk: () => {
+ this.handleUploadTrack(change)
+ },
+ onCancel: () => {
+ this.handleTrackRemove(change.file.uid)
+ }
+ })
+ }
+ case "removed": {
+ this.handleTrackRemove(change.file.uid)
+ }
+
+ default: {
+ break
+ }
+ }
+ }
+
+ async function handleUploadTrack (req) {
+ const response = await app.cores.remoteStorage.uploadFile(req.file, {
+ onProgress: this.handleFileProgress,
+ service: "premium-cdn"
+ }).catch((error) => {
+ console.error(error)
+ antd.message.error(error)
+
+ req.onError(error)
+
+ return false
+ })
+
+ if (response) {
+ req.onSuccess(response)
+ }
+ }
+
+ async function onTrackDragEnd(result) {
+ console.log(result)
+
+ if (!result.destination) {
+ return
+ }
+
+ setList((prev) => {
+ const trackList = [...prev]
+
+ const [removed] = trackList.splice(result.source.index, 1)
+
+ trackList.splice(result.destination.index, 0, removed)
+
+ return trackList
+ })
+ }
+
+ return
+
Tracks
+
+
+
+ {
+ list.length === 0 ?
+ : }
+ />
+ }
+
+
+
+
+ {(provided, snapshot) => (
+
+ {
+ list.length === 0 &&
+ }
+ {
+ list.map((track, index) => {
+ return
+ })
+ }
+ {provided.placeholder}
+
+ )}
+
+
+
+
+}
+
+export default ReleaseTracks
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/index.less b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/index.less
new file mode 100644
index 00000000..08b787f4
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/Tracks/index.less
@@ -0,0 +1,52 @@
+.music-studio-release-editor-tracks-list {
+ display: flex;
+ flex-direction: column;
+
+ gap: 10px;
+}
+
+.music-studio-release-editor-tracks-list-item {
+ position: relative;
+
+ display: flex;
+ flex-direction: row;
+
+ padding: 10px;
+
+ gap: 10px;
+
+ border-radius: 12px;
+
+ background-color: var(--background-color-accent);
+
+ .music-studio-release-editor-tracks-list-item-actions {
+ position: absolute;
+
+ top: 0;
+ right: 0;
+
+ display: flex;
+
+ align-items: center;
+ justify-content: center;
+
+ height: 100%;
+
+ padding: 0 5px;
+
+ svg {
+ margin: 0;
+ }
+
+ .music-studio-release-editor-tracks-list-item-dragger {
+ display: flex;
+
+ align-items: center;
+ justify-content: center;
+
+ svg {
+ font-size: 1rem;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/index.jsx b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/index.jsx
new file mode 100644
index 00000000..666d1599
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/ReleaseEditor/tabs/index.jsx
@@ -0,0 +1,26 @@
+import { Icons, createIconRender } from "@components/Icons"
+
+import BasicInformation from "./BasicInformation"
+import Tracks from "./Tracks"
+import Advanced from "./Advanced"
+
+export default [
+ {
+ key: "info",
+ label: "Info",
+ icon: ,
+ render: BasicInformation,
+ },
+ {
+ key: "tracks",
+ label: "Tracks",
+ icon: ,
+ render: Tracks,
+ },
+ {
+ key: "advanced",
+ label: "Advanced",
+ icon: ,
+ render: Advanced,
+ }
+]
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/ReleaseItem/index.jsx b/packages/app/src/components/MusicStudio/ReleaseItem/index.jsx
new file mode 100644
index 00000000..431f6a15
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/ReleaseItem/index.jsx
@@ -0,0 +1,51 @@
+import React from "react"
+
+import { Icons } from "@components/Icons"
+import Image from "@components/Image"
+
+import "./index.less"
+
+const ReleaseItem = (props) => {
+ const { release, onClick } = props
+
+ async function handleOnClick() {
+ if (typeof onClick === "function") {
+ return onClick(release)
+ }
+ }
+
+ return
+
+
+
+ {release.title}
+
+
+
+
+
+ {release.type}
+
+
+
+
+ {release._id}
+
+
+ {/*
+
+ {release.analytics?.listen_count ?? 0}
+
*/}
+
+
+}
+
+export default ReleaseItem
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/ReleaseItem/index.less b/packages/app/src/components/MusicStudio/ReleaseItem/index.less
new file mode 100644
index 00000000..115c74d7
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/ReleaseItem/index.less
@@ -0,0 +1,71 @@
+.music-studio-page-release {
+ display: flex;
+ flex-direction: column;
+
+ justify-content: center;
+
+ width: 100%;
+
+ padding: 7px;
+
+ gap: 7px;
+
+ background-color: var(--background-color-accent);
+
+ border-radius: 12px;
+
+ transition: all 150ms ease-in-out;
+
+ &:hover {
+ cursor: pointer;
+ background-color: var(--background-color-accent-hover);
+ outline: 2px solid var(--border-color);
+ }
+
+ .music-studio-page-release-title {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+
+ gap: 10px;
+
+ .lazy-load-image-background {
+ width: 40px;
+ height: 40px;
+
+ border-radius: 8px;
+
+ overflow: hidden;
+
+ img {
+ width: 100%;
+ height: 100%;
+ }
+ }
+ }
+
+ .music-studio-page-release-info {
+ display: flex;
+ flex-direction: row;
+
+ gap: 10px;
+
+ text-transform: uppercase;
+
+ font-size: 12px;
+
+ .music-studio-page-release-info-field {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+
+ gap: 5px;
+
+ svg {
+ margin: 0;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/TrackEditor/index.jsx b/packages/app/src/components/MusicStudio/TrackEditor/index.jsx
new file mode 100644
index 00000000..ce290e28
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/TrackEditor/index.jsx
@@ -0,0 +1,208 @@
+import React from "react"
+import * as antd from "antd"
+
+import CoverEditor from "@components/CoverEditor"
+import { Icons } from "@components/Icons"
+
+import LyricsEditor from "@components/MusicStudio/LyricsEditor"
+import VideoEditor from "@components/MusicStudio/VideoEditor"
+
+import "./index.less"
+
+const TrackEditor = (props) => {
+ const [track, setTrack] = React.useState(props.track ?? {})
+
+ async function handleChange(key, value) {
+ setTrack((prev) => {
+ return {
+ ...prev,
+ [key]: value
+ }
+ })
+ }
+
+ async function openLyricsEditor() {
+ app.layout.drawer.open("lyrics_editor", LyricsEditor, {
+ type: "drawer",
+ props: {
+ width: "600px",
+ headerStyle: {
+ display: "none",
+ }
+ },
+ componentProps: {
+ track,
+ onSave: (lyrics) => {
+ console.log("Saving lyrics for track >", lyrics)
+ },
+ },
+ })
+ }
+
+ async function openVideoEditor() {
+ app.layout.drawer.open("video_editor", VideoEditor, {
+ type: "drawer",
+ props: {
+ width: "600px",
+ headerStyle: {
+ display: "none",
+ }
+ },
+ componentProps: {
+ track,
+ onSave: (video) => {
+ console.log("Saving video for track", video)
+ },
+ },
+ })
+ }
+
+ async function onClose() {
+ if (typeof props.close === "function") {
+ props.close()
+ }
+ }
+
+ async function onSave() {
+ await props.onSave(track)
+
+ if (typeof props.close === "function") {
+ props.close()
+ }
+ }
+
+ return
+
+
+
+ Cover
+
+
+
handleChange("cover", url)}
+ extraActions={[
+
+ Use Parent
+
+ ]}
+ />
+
+
+
+
+
+ Title
+
+
+
handleChange("title", e.target.value)}
+ />
+
+
+
+
+
+ Artist
+
+
+
handleChange("artist", e.target.value)}
+ />
+
+
+
+
+
+ Album
+
+
+
handleChange("album", e.target.value)}
+ />
+
+
+
+
+
+ Explicit
+
+
+
handleChange("explicit", value)}
+ />
+
+
+
+
+
+
+
+ Edit Video
+
+
+
+ Edit
+
+
+
+
+
+
+ Edit Lyrics
+
+
+
+ Edit
+
+
+
+
+
+
+ Timestamps
+
+
+
+ Edit
+
+
+
+
+
}
+ onClick={onClose}
+ >
+ Cancel
+
+
+
}
+ onClick={onSave}
+ >
+ Save
+
+
+
+}
+
+export default TrackEditor
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/TrackEditor/index.less b/packages/app/src/components/MusicStudio/TrackEditor/index.less
new file mode 100644
index 00000000..f6570bb8
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/TrackEditor/index.less
@@ -0,0 +1,57 @@
+.track-editor {
+ display: flex;
+ flex-direction: column;
+
+ align-items: center;
+
+ gap: 20px;
+
+ .track-editor-actions {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+ justify-content: flex-end;
+
+ align-self: center;
+
+ gap: 10px;
+ }
+
+ .track-editor-field {
+ display: flex;
+ flex-direction: column;
+
+ align-items: flex-start;
+
+ gap: 10px;
+
+ width: 100%;
+
+ .track-editor-field-header {
+ display: inline-flex;
+ flex-direction: row;
+
+ justify-content: flex-start;
+ align-items: center;
+
+ width: 100%;
+
+ h3 {
+ font-size: 1.2rem;
+ }
+ }
+
+ .track-editor-field-actions {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+ justify-content: center;
+
+ gap: 10px;
+
+ width: 100%;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/VideoEditor/index.jsx b/packages/app/src/components/MusicStudio/VideoEditor/index.jsx
new file mode 100644
index 00000000..f95f1ef4
--- /dev/null
+++ b/packages/app/src/components/MusicStudio/VideoEditor/index.jsx
@@ -0,0 +1,14 @@
+import React from "react"
+import * as antd from "antd"
+
+import { Icons } from "@components/Icons"
+
+import "./index.less"
+
+const VideoEditor = (props) => {
+ return
+
+
+}
+
+export default VideoEditor
\ No newline at end of file
diff --git a/packages/app/src/components/MusicStudio/VideoEditor/index.less b/packages/app/src/components/MusicStudio/VideoEditor/index.less
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/app/src/components/PagePanels/index.jsx b/packages/app/src/components/PagePanels/index.jsx
index 13bde614..1ce47613 100755
--- a/packages/app/src/components/PagePanels/index.jsx
+++ b/packages/app/src/components/PagePanels/index.jsx
@@ -8,6 +8,31 @@ import NavMenu from "./components/NavMenu"
import "./index.less"
+export class Tab extends React.Component {
+ state = {
+ error: null
+ }
+
+ // handle on error
+ componentDidCatch(err) {
+ this.setState({ error: err })
+ }
+
+ render() {
+ if (this.state.error) {
+ return
+ }
+
+ return <>
+ {this.props.children}
+ >
+ }
+}
+
export const Panel = (props) => {
return {
trigger={["click"]}
onOpenChange={(open) => {
if (open && props.user_id) {
- const isSelf = app.cores.permissions.checkUserIdIsSelf(props.user_id)
+ const isSelf = checkUserIdIsSelf(props.user_id)
setIsSelf(isSelf)
}
diff --git a/packages/app/src/components/PostCard/components/header/index.jsx b/packages/app/src/components/PostCard/components/header/index.jsx
index 948b12c6..60d0121a 100755
--- a/packages/app/src/components/PostCard/components/header/index.jsx
+++ b/packages/app/src/components/PostCard/components/header/index.jsx
@@ -2,6 +2,7 @@ import React from "react"
import { DateTime } from "luxon"
import { Tag } from "antd"
+import TimeAgo from "@components/TimeAgo"
import Image from "@components/Image"
import { Icons } from "@components/Icons"
@@ -10,37 +11,10 @@ import PostReplieView from "@components/PostReplieView"
import "./index.less"
const PostCardHeader = (props) => {
- const [timeAgo, setTimeAgo] = React.useState(0)
-
const goToProfile = () => {
app.navigation.goToAccount(props.postData.user?.username)
}
- const updateTimeAgo = () => {
- let createdAt = props.postData.timestamp ?? props.postData.created_at ?? ""
-
- const timeAgo = DateTime.fromISO(
- createdAt,
- {
- locale: app.cores.settings.get("language")
- }
- ).toRelative()
-
- setTimeAgo(timeAgo)
- }
-
- React.useEffect(() => {
- updateTimeAgo()
-
- const interval = setInterval(() => {
- updateTimeAgo()
- }, 1000 * 60 * 5)
-
- return () => {
- clearInterval(interval)
- }
- }, [])
-
return
{
!props.disableReplyTag && props.postData.reply_to &&
{
- {timeAgo}
+
diff --git a/packages/app/src/components/ReleasesList/index.jsx b/packages/app/src/components/ReleasesList/index.jsx
new file mode 100644
index 00000000..df46425b
--- /dev/null
+++ b/packages/app/src/components/ReleasesList/index.jsx
@@ -0,0 +1,132 @@
+import React from "react"
+import * as antd from "antd"
+import { Translation } from "react-i18next"
+
+import { Icons } from "@components/Icons"
+import PlaylistItem from "@components/Music/PlaylistItem"
+
+import "./index.less"
+
+const ReleasesList = (props) => {
+ const hopNumber = props.hopsPerPage ?? 6
+
+ const [offset, setOffset] = React.useState(0)
+ const [ended, setEnded] = React.useState(false)
+
+ const [loading, result, error, makeRequest] = app.cores.api.useRequest(props.fetchMethod, {
+ limit: hopNumber,
+ trim: offset
+ })
+
+ const onClickPrev = () => {
+ if (offset === 0) {
+ return
+ }
+
+ setOffset((value) => {
+ const newOffset = value - hopNumber
+
+ // check if newOffset is NaN
+ if (newOffset !== newOffset) {
+ return false
+ }
+
+ if (typeof makeRequest === "function") {
+ makeRequest({
+ trim: newOffset,
+ limit: hopNumber,
+ })
+ }
+
+ return newOffset
+ })
+ }
+
+ const onClickNext = () => {
+ if (ended) {
+ return
+ }
+
+ setOffset((value) => {
+ const newOffset = value + hopNumber
+
+ // check if newOffset is NaN
+ if (newOffset !== newOffset) {
+ return false
+ }
+
+ if (typeof makeRequest === "function") {
+ makeRequest({
+ trim: newOffset,
+ limit: hopNumber,
+ })
+ }
+
+ return newOffset
+ })
+ }
+
+ React.useEffect(() => {
+ if (result) {
+ if (typeof result.has_more !== "undefined") {
+ setEnded(!result.has_more)
+ } else {
+ setEnded(result.items.length < hopNumber)
+ }
+ }
+ }, [result])
+
+ if (error) {
+ console.error(error)
+
+ return
+ }
+
+ return
+
+
+ {
+ props.headerIcon
+ }
+
+ {(t) => t(props.headerTitle)}
+
+
+
+
+
}
+ onClick={onClickPrev}
+ disabled={offset === 0 || loading}
+ />
+
+
}
+ onClick={onClickNext}
+ disabled={ended || loading}
+ />
+
+
+
+ {
+ loading &&
+ }
+ {
+ !loading && result.items.map((playlist, index) => {
+ return
+ })
+ }
+
+
+}
+
+export default ReleasesList
\ No newline at end of file
diff --git a/packages/app/src/components/ReleasesList/index.less b/packages/app/src/components/ReleasesList/index.less
new file mode 100644
index 00000000..7ec79333
--- /dev/null
+++ b/packages/app/src/components/ReleasesList/index.less
@@ -0,0 +1,52 @@
+.music-releases-list {
+ display: flex;
+ flex-direction: column;
+
+ overflow-x: visible;
+
+ .music-releases-list-header {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+
+ margin-bottom: 20px;
+
+ h1 {
+ font-size: 1.5rem;
+ margin: 0;
+ }
+
+ .music-releases-list-actions {
+ display: flex;
+ flex-direction: row;
+
+ gap: 10px;
+
+ align-self: center;
+
+ margin-left: auto;
+ }
+ }
+
+ .music-releases-list-items {
+ display: grid;
+
+ grid-gap: 20px;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+
+ min-width: 372px !important;
+
+ @media (min-width: 2000px) {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ @media (min-width: 2300px) {
+ grid-template-columns: repeat(5, 1fr);
+ }
+
+ .playlistItem {
+ justify-self: center;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/TimeAgo/index.jsx b/packages/app/src/components/TimeAgo/index.jsx
new file mode 100644
index 00000000..0d446b88
--- /dev/null
+++ b/packages/app/src/components/TimeAgo/index.jsx
@@ -0,0 +1,33 @@
+import React from "react"
+
+import { DateTime } from "luxon"
+
+const TimeAgo = (props) => {
+ const [calculationInterval, setCalculationInterval] = React.useState(null)
+ const [text, setText] = React.useState("")
+
+ async function calculateRelative() {
+ const timeAgo = DateTime.fromISO(
+ props.time,
+ {
+ locale: app.cores.settings.get("language")
+ }
+ ).toRelative()
+
+ setText(timeAgo)
+ }
+
+ React.useEffect(() => {
+ setCalculationInterval(setInterval(calculateRelative, props.interval ?? 3000))
+
+ calculateRelative()
+
+ return () => {
+ clearInterval(calculationInterval)
+ }
+ }, [])
+
+ return text
+}
+
+export default TimeAgo
\ No newline at end of file
diff --git a/packages/app/src/components/UserCard/index.jsx b/packages/app/src/components/UserCard/index.jsx
index 9a5daf23..dfebc520 100755
--- a/packages/app/src/components/UserCard/index.jsx
+++ b/packages/app/src/components/UserCard/index.jsx
@@ -54,7 +54,7 @@ const UserLink = (props) => {
const handleOnClick = () => {
if (!hasHref) {
if (app.isMobile) {
- app.DrawerController.open("link_viewer", UserLinkViewer, {
+ app.layout.drawer.open("link_viewer", UserLinkViewer, {
componentProps: {
link: link,
decorator: decorator
diff --git a/packages/app/src/cores/api/api.core.js b/packages/app/src/cores/api/api.core.js
index c791ef4a..d3f4d8e1 100755
--- a/packages/app/src/cores/api/api.core.js
+++ b/packages/app/src/cores/api/api.core.js
@@ -30,7 +30,7 @@ export default class APICore extends Core {
listenEvent(key, handler, instance = "default") {
if (!this.client.sockets[instance]) {
- console.error(`[API] Websocket instance ${instance} not found`)
+ this.console.error(`[API] Websocket instance ${instance} not found`)
return false
}
@@ -40,7 +40,7 @@ export default class APICore extends Core {
unlistenEvent(key, handler, instance = "default") {
if (!this.client.sockets[instance]) {
- console.error(`[API] Websocket instance ${instance} not found`)
+ this.console.error(`[API] Websocket instance ${instance} not found`)
return false
}
diff --git a/packages/app/src/cores/nfc/nfc.core.js b/packages/app/src/cores/nfc/nfc.core.js
index a8bb15ab..6d274157 100755
--- a/packages/app/src/cores/nfc/nfc.core.js
+++ b/packages/app/src/cores/nfc/nfc.core.js
@@ -161,7 +161,7 @@ export default class NFC extends Core {
if (this.subscribers.length === 0) {
if (tag.message.records?.length > 0) {
// open dialog
- app.DrawerController.open("nfc_card_dialog", TapShareDialog, {
+ app.layout.drawer.open("nfc_card_dialog", TapShareDialog, {
componentProps: {
tag: tag,
}
@@ -187,7 +187,7 @@ export default class NFC extends Core {
if (this.subscribers.length === 0 && tag.message?.records) {
if (tag.message.records?.length > 0) {
// open dialog
- app.DrawerController.open("nfc_card_dialog", TapShareDialog, {
+ app.layout.drawer.open("nfc_card_dialog", TapShareDialog, {
componentProps: {
tag: tag,
}
diff --git a/packages/app/src/cores/permissions/permissions.core.js b/packages/app/src/cores/permissions/permissions.core.js
deleted file mode 100755
index e2fbae17..00000000
--- a/packages/app/src/cores/permissions/permissions.core.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import Core from "evite/src/core"
-
-import UserModel from "@models/user"
-import SessionModel from "@models/session"
-
-export default class PermissionsCore extends Core {
- static namespace = "permissions"
-
- static dependencies = ["api"]
-
- public = {
- getRoles: this.getRoles,
- hasAdmin: this.hasAdmin,
- checkUserIdIsSelf: this.checkUserIdIsSelf,
- hasPermission: this.hasPermission,
- }
-
- async hasAdmin() {
- return await UserModel.haveAdmin()
- }
-
- checkUserIdIsSelf(user_id) {
- return SessionModel.user_id === user_id
- }
-
- async getRoles() {
- return await UserModel.selfRoles()
- }
-
- async hasPermission(permission, adminPreference = false) {
- if (adminPreference) {
- const admin = await this.hasAdmin()
-
- if (admin) {
- return true
- }
- }
-
- let query = []
-
- if (Array.isArray(permission)) {
- query = permission
- } else {
- query = [permission]
- }
-
- // create a promise and check if the user has all the permission in the query
- const result = await Promise.all(query.map(async (permission) => {
- const hasPermission = await UserModel.haveRole(permission)
-
- return hasPermission
- }))
-
- // if the user has all the permission in the query, return true
- if (result.every((hasPermission) => hasPermission)) {
- return true
- }
-
- return false
- }
-}
\ No newline at end of file
diff --git a/packages/app/src/cores/player/classes/TrackInstance.js b/packages/app/src/cores/player/classes/TrackInstance.js
new file mode 100644
index 00000000..7915308e
--- /dev/null
+++ b/packages/app/src/cores/player/classes/TrackInstance.js
@@ -0,0 +1,127 @@
+export default class TrackInstance {
+ constructor(player, manifest) {
+ if (!player) {
+ throw new Error("Player core is required")
+ }
+
+ if (typeof manifest === "undefined") {
+ throw new Error("Manifest is required")
+ }
+
+ this.player = player
+ this.manifest = manifest
+
+ return this
+ }
+
+ audio = null
+
+ contextElement = null
+
+ abortController = new AbortController()
+
+ attachedProcessors = []
+
+ waitUpdateTimeout = null
+
+ resolveManifest = async () => {
+ if (typeof this.manifest === "string") {
+ this.manifest = {
+ src: this.manifest,
+ }
+ }
+
+ if (this.manifest.service) {
+ if (!this.player.service_providers.has(manifest.service)) {
+ throw new Error(`Service ${manifest.service} is not supported`)
+ }
+
+ // try to resolve source file
+ if (this.manifest.service !== "inherit" && !this.manifest.source) {
+ this.manifest = await this.player.service_providers.resolve(this.manifest.service, this.manifest)
+ }
+ }
+
+ if (!this.manifest.source) {
+ throw new Error("Manifest `source` is required")
+ }
+
+ if (!this.manifest.metadata) {
+ this.manifest.metadata = {}
+ }
+
+ if (!this.manifest.metadata.title) {
+ this.manifest.metadata.title = this.manifest.source.split("/").pop()
+ }
+
+ return this.manifest
+ }
+
+ initialize = async () => {
+ this.manifest = await this.resolveManifest()
+
+ this.audio = new Audio(this.manifest.source)
+
+ this.audio.signal = this.abortController.signal
+ this.audio.crossOrigin = "anonymous"
+ this.audio.preload = "metadata"
+
+ for (const [key, value] of Object.entries(this.mediaEvents)) {
+ this.audio.addEventListener(key, value)
+ }
+
+ this.contextElement = this.player.audioContext.createMediaElementSource(this.audio)
+
+ return this
+ }
+
+ mediaEvents = {
+ "ended": () => {
+ this.player.next()
+ },
+ "loadeddata": () => {
+ this.player.state.loading = false
+ },
+ "loadedmetadata": () => {
+ // TODO: Detect a livestream and change mode
+ // if (instance.media.duration === Infinity) {
+ // instance.manifest.stream = true
+
+ // this.state.livestream_mode = true
+ // }
+ },
+ "play": () => {
+ this.player.state.playback_status = "playing"
+ },
+ "playing": () => {
+ this.player.state.loading = false
+
+ this.player.state.playback_status = "playing"
+
+ if (typeof this.waitUpdateTimeout !== "undefined") {
+ clearTimeout(this.waitUpdateTimeout)
+ this.waitUpdateTimeout = null
+ }
+ },
+ "pause": () => {
+ this.player.state.playback_status = "paused"
+ },
+ // "durationchange": (duration) => {
+
+ // },
+ "waiting": () => {
+ if (this.waitUpdateTimeout) {
+ clearTimeout(this.waitUpdateTimeout)
+ this.waitUpdateTimeout = null
+ }
+
+ // if takes more than 150ms to load, update loading state
+ this.waitUpdateTimeout = setTimeout(() => {
+ this.player.state.loading = true
+ }, 150)
+ },
+ "seeked": () => {
+ this.player.eventBus.emit(`player.seeked`, this.audio.currentTime)
+ },
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/cores/player/player.bkp.js b/packages/app/src/cores/player/player.bkp.js
new file mode 100755
index 00000000..356bbe17
--- /dev/null
+++ b/packages/app/src/cores/player/player.bkp.js
@@ -0,0 +1,947 @@
+import Core from "evite/src/core"
+import EventEmitter from "evite/src/internals/EventEmitter"
+import { Observable } from "object-observer"
+import { FastAverageColor } from "fast-average-color"
+
+import MusicModel from "comty.js/models/music"
+
+import ToolBarPlayer from "@components/Player/ToolBarPlayer"
+import BackgroundMediaPlayer from "@components/Player/BackgroundMediaPlayer"
+
+import AudioPlayerStorage from "./player.storage"
+
+import defaultAudioProccessors from "./processors"
+
+import MediaSession from "./mediaSession"
+import ServiceProviders from "./services"
+
+export default class Player extends Core {
+ static dependencies = [
+ "api",
+ "settings"
+ ]
+
+ static namespace = "player"
+
+ static bgColor = "aquamarine"
+ static textColor = "black"
+
+ static defaultSampleRate = 48000
+
+ static gradualFadeMs = 150
+
+ // buffer & precomputation
+ static maxManifestPrecompute = 3
+
+ service_providers = new ServiceProviders()
+
+ native_controls = new MediaSession()
+
+ currentDomWindow = null
+
+ audioContext = new AudioContext({
+ sampleRate: AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate,
+ latencyHint: "playback"
+ })
+
+ audioProcessors = []
+
+ eventBus = new EventEmitter()
+
+ fac = new FastAverageColor()
+
+ track_prev_instances = []
+ track_instance = null
+ track_next_instances = []
+
+ state = Observable.from({
+ loading: false,
+ minimized: false,
+
+ muted: app.isMobile ? false : (AudioPlayerStorage.get("mute") ?? false),
+ volume: app.isMobile ? 1 : (AudioPlayerStorage.get("volume") ?? 0.3),
+
+ sync_mode: false,
+ livestream_mode: false,
+ control_locked: false,
+
+ track_manifest: null,
+
+ playback_mode: AudioPlayerStorage.get("mode") ?? "normal",
+ playback_status: "stopped",
+ })
+
+ public = {
+ audioContext: this.audioContext,
+ setSampleRate: this.setSampleRate,
+ start: this.start.bind(this),
+ close: this.close.bind(this),
+ playback: {
+ mode: this.playbackMode.bind(this),
+ stop: this.stop.bind(this),
+ toggle: this.togglePlayback.bind(this),
+ pause: this.pausePlayback.bind(this),
+ play: this.resumePlayback.bind(this),
+ next: this.next.bind(this),
+ previous: this.previous.bind(this),
+ seek: this.seek.bind(this),
+ },
+ _setLoading: function (to) {
+ this.state.loading = !!to
+ }.bind(this),
+ duration: this.duration.bind(this),
+ volume: this.volume.bind(this),
+ mute: this.mute.bind(this),
+ toggleMute: this.toggleMute.bind(this),
+ seek: this.seek.bind(this),
+ minimize: this.toggleMinimize.bind(this),
+ collapse: this.toggleCollapse.bind(this),
+ state: new Proxy(this.state, {
+ get: (target, prop) => {
+ return target[prop]
+ },
+ set: (target, prop, value) => {
+ return false
+ }
+ }),
+ eventBus: new Proxy(this.eventBus, {
+ get: (target, prop) => {
+ return target[prop]
+ },
+ set: (target, prop, value) => {
+ return false
+ }
+ }),
+ gradualFadeMs: Player.gradualFadeMs,
+ trackInstance: () => {
+ return this.track_instance
+ }
+ }
+
+ internalEvents = {
+ "player.state.update:loading": () => {
+ //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state)
+ },
+ "player.state.update:track_manifest": () => {
+ //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state)
+ },
+ "player.state.update:playback_status": () => {
+ //app.cores.sync.music.dispatchEvent("music.player.state.update", this.state)
+ },
+ "player.seeked": (to) => {
+ //app.cores.sync.music.dispatchEvent("music.player.seek", to)
+ },
+ }
+
+ async onInitialize() {
+ this.native_controls.initialize()
+
+ this.initializeAudioProcessors()
+
+ for (const [eventName, eventHandler] of Object.entries(this.internalEvents)) {
+ this.eventBus.on(eventName, eventHandler)
+ }
+
+ Observable.observe(this.state, async (changes) => {
+ try {
+ changes.forEach((change) => {
+ if (change.type === "update") {
+ const stateKey = change.path[0]
+
+ this.eventBus.emit(`player.state.update:${stateKey}`, change.object[stateKey])
+ this.eventBus.emit("player.state.update", change.object)
+ }
+ })
+ } catch (error) {
+ this.console.error(`Failed to dispatch state updater >`, error)
+ }
+ })
+ }
+
+ async initializeBeforeRuntimeInitialize() {
+ for (const [eventName, eventHandler] of Object.entries(this.wsEvents)) {
+ app.cores.api.listenEvent(eventName, eventHandler, Player.websocketListen)
+ }
+
+ if (app.isMobile) {
+ this.state.audioVolume = 1
+ }
+ }
+
+ async initializeAudioProcessors() {
+ if (this.audioProcessors.length > 0) {
+ this.console.log("Destroying audio processors")
+
+ this.audioProcessors.forEach((processor) => {
+ this.console.log(`Destroying audio processor ${processor.constructor.name}`, processor)
+ processor._destroy()
+ })
+
+ this.audioProcessors = []
+ }
+
+ for await (const defaultProccessor of defaultAudioProccessors) {
+ this.audioProcessors.push(new defaultProccessor(this))
+ }
+
+ for await (const processor of this.audioProcessors) {
+ if (typeof processor._init === "function") {
+ try {
+ await processor._init(this.audioContext)
+ } catch (error) {
+ this.console.error(`Failed to initialize audio processor ${processor.constructor.name} >`, error)
+ continue
+ }
+ }
+
+ // check if processor has exposed public methods
+ if (processor.exposeToPublic) {
+ Object.entries(processor.exposeToPublic).forEach(([key, value]) => {
+ const refName = processor.constructor.refName
+
+ if (typeof this.public[refName] === "undefined") {
+ // by default create a empty object
+ this.public[refName] = {}
+ }
+
+ this.public[refName][key] = value
+ })
+ }
+ }
+ }
+
+ //
+ // UI Methods
+ //
+
+ attachPlayerComponent() {
+ if (this.currentDomWindow) {
+ this.console.warn("EmbbededMediaPlayer already attached")
+ return false
+ }
+
+ if (app.layout.tools_bar) {
+ this.currentDomWindow = app.layout.tools_bar.attachRender("mediaPlayer", ToolBarPlayer)
+ }
+
+ }
+
+ detachPlayerComponent() {
+ if (!this.currentDomWindow) {
+ this.console.warn("EmbbededMediaPlayer not attached")
+ return false
+ }
+
+ if (!app.layout.tools_bar) {
+ this.console.error("Tools bar not found")
+ return false
+ }
+
+ app.layout.tools_bar.detachRender("mediaPlayer")
+
+ this.currentDomWindow = null
+ }
+
+ //
+ // Instance managing methods
+ //
+ async abortPreloads() {
+ for await (const instance of this.track_next_instances) {
+ if (instance.abortController?.abort) {
+ instance.abortController.abort()
+ }
+ }
+ }
+
+ async preloadAudioInstance(instance) {
+ const isIndex = typeof instance === "number"
+
+ let index = isIndex ? instance : 0
+
+ if (isIndex) {
+ instance = this.track_next_instances[instance]
+ }
+
+ if (!instance) {
+ this.console.error("Instance not found to preload")
+ return false
+ }
+
+ if (!instance.manifest.cover_analysis) {
+ const cover_analysis = await this.fac.getColorAsync(`https://corsproxy.io/?${encodeURIComponent(instance.manifest.cover ?? instance.manifest.thumbnail)}`)
+ .catch((err) => {
+ this.console.error(err)
+
+ return false
+ })
+
+ instance.manifest.cover_analysis = cover_analysis
+ }
+
+ if (!instance._preloaded) {
+ instance.media.preload = "metadata"
+ instance._preloaded = true
+ }
+
+ if (isIndex) {
+ this.track_next_instances[index] = instance
+ }
+
+ return instance
+ }
+
+ async destroyCurrentInstance({ sync = false } = {}) {
+ if (!this.track_instance) {
+ return false
+ }
+
+ // stop playback
+ if (this.track_instance.media) {
+ this.track_instance.media.pause()
+ }
+
+ // reset track_instance
+ this.track_instance = null
+
+ // reset livestream mode
+ this.state.livestream_mode = false
+ }
+
+ async createInstance(manifest) {
+ if (!manifest) {
+ this.console.error("Manifest is required")
+ return false
+ }
+
+ if (typeof manifest === "string") {
+ manifest = {
+ src: manifest,
+ }
+ }
+
+ // check if manifest has `manifest` property, if is and not inherit or missing source, resolve
+ if (manifest.service) {
+ if (!this.service_providers.has(manifest.service)) {
+ this.console.error(`Service ${manifest.service} is not supported`)
+ return false
+ }
+
+ if (manifest.service !== "inherit" && !manifest.source) {
+ manifest = await this.service_providers.resolve(manifest.service, manifest)
+ }
+ }
+
+ if (!manifest.src && !manifest.source) {
+ this.console.error("Manifest source is required")
+ return false
+ }
+
+ const source = manifest.src ?? manifest.source
+
+ if (!manifest.metadata) {
+ manifest.metadata = {}
+ }
+
+ // if title is not set, use the audio source filename
+ if (!manifest.metadata.title) {
+ manifest.metadata.title = source.split("/").pop()
+ }
+
+ let instance = {
+ manifest: manifest,
+ attachedProcessors: [],
+ abortController: new AbortController(),
+ source: source,
+ media: new Audio(source),
+ duration: null,
+ seek: 0,
+ track: null,
+ }
+
+ instance.media.signal = instance.abortController.signal
+ instance.media.crossOrigin = "anonymous"
+ instance.media.preload = "metadata"
+
+ instance.media.loop = this.state.playback_mode === "repeat"
+ instance.media.volume = this.state.volume
+
+ // handle on end
+ instance.media.addEventListener("ended", () => {
+ this.next()
+ })
+
+ instance.media.addEventListener("loadeddata", () => {
+ this.state.loading = false
+ })
+
+ // update playback status
+ instance.media.addEventListener("play", () => {
+ this.state.playback_status = "playing"
+ })
+
+ instance.media.addEventListener("playing", () => {
+ this.state.loading = false
+
+ this.state.playback_status = "playing"
+
+ if (this.waitUpdateTimeout) {
+ clearTimeout(this.waitUpdateTimeout)
+ this.waitUpdateTimeout = null
+ }
+ })
+
+ instance.media.addEventListener("pause", () => {
+ this.state.playback_status = "paused"
+ })
+
+ instance.media.addEventListener("durationchange", (duration) => {
+ if (instance.media.paused) {
+ return false
+ }
+
+ instance.duration = duration
+ })
+
+ instance.media.addEventListener("waiting", () => {
+ if (instance.media.paused) {
+ return false
+ }
+
+ if (this.waitUpdateTimeout) {
+ clearTimeout(this.waitUpdateTimeout)
+ this.waitUpdateTimeout = null
+ }
+
+ // if takes more than 150ms to load, update loading state
+ this.waitUpdateTimeout = setTimeout(() => {
+ this.state.loading = true
+ }, 150)
+ })
+
+ instance.media.addEventListener("seeked", () => {
+ this.console.log(`Seeked to ${instance.seek}`)
+
+ this.eventBus.emit(`player.seeked`, instance.seek)
+ })
+
+ instance.media.addEventListener("loadedmetadata", () => {
+ if (instance.media.duration === Infinity) {
+ instance.manifest.stream = true
+
+ this.state.livestream_mode = true
+ }
+ }, { once: true })
+
+ instance.track = this.audioContext.createMediaElementSource(instance.media)
+
+ return instance
+ }
+
+ async attachProcessorsToInstance(instance) {
+ for await (const [index, processor] of this.audioProcessors.entries()) {
+ if (processor.constructor.node_bypass === true) {
+ instance.track.connect(processor.processor)
+
+ processor.processor.connect(this.audioContext.destination)
+
+ continue
+ }
+
+ if (typeof processor._attach !== "function") {
+ this.console.error(`Processor ${processor.constructor.refName} not support attach`)
+
+ continue
+ }
+
+ instance = await processor._attach(instance, index)
+ }
+
+ const lastProcessor = instance.attachedProcessors[instance.attachedProcessors.length - 1].processor
+
+ // now attach to destination
+ lastProcessor.connect(this.audioContext.destination)
+
+ return instance
+ }
+
+ //
+ // Playback methods
+ //
+ async play(instance, params = {}) {
+ if (typeof instance === "number") {
+ if (instance < 0) {
+ instance = this.track_prev_instances[instance]
+ }
+
+ if (instance > 0) {
+ instance = this.track_instances[instance]
+ }
+
+ if (instance === 0) {
+ instance = this.track_instance
+ }
+ }
+
+ if (!instance) {
+ throw new Error("Audio instance is required")
+ }
+
+ if (this.audioContext.state === "suspended") {
+ this.audioContext.resume()
+ }
+
+ if (this.track_instance) {
+ this.track_instance = this.track_instance.attachedProcessors[this.track_instance.attachedProcessors.length - 1]._destroy(this.track_instance)
+
+ this.destroyCurrentInstance()
+ }
+
+ // attach processors
+ instance = await this.attachProcessorsToInstance(instance)
+
+ // now set the current instance
+ this.track_instance = await this.preloadAudioInstance(instance)
+
+ // reconstruct audio src if is not set
+ if (this.track_instance.media.src !== instance.source) {
+ this.track_instance.media.src = instance.source
+ }
+
+ // set time to 0
+ this.track_instance.media.currentTime = 0
+
+ if (params.time >= 0) {
+ this.track_instance.media.currentTime = params.time
+ }
+
+ this.track_instance.media.muted = this.state.muted
+ this.track_instance.media.loop = this.state.playback_mode === "repeat"
+
+ // try to preload next audio
+ // TODO: Use a better way to preload queues
+ if (this.track_next_instances.length > 0) {
+ this.preloadAudioInstance(1)
+ }
+
+ // play
+ await this.track_instance.media.play()
+
+ this.console.debug(`Playing track >`, this.track_instance)
+
+ // update manifest
+ this.state.track_manifest = instance.manifest
+
+ this.native_controls.update(instance.manifest)
+
+ return this.track_instance
+ }
+
+ async start(manifest, { sync = false, time, startIndex = 0 } = {}) {
+ if (this.state.control_locked && !sync) {
+ this.console.warn("Controls are locked, cannot do this action")
+ return false
+ }
+
+ this.attachPlayerComponent()
+
+ // !IMPORTANT: abort preloads before destroying current instance
+ await this.abortPreloads()
+ await this.destroyCurrentInstance({
+ sync
+ })
+
+ this.state.loading = true
+
+ this.track_prev_instances = []
+ this.track_next_instances = []
+
+ let playlist = Array.isArray(manifest) ? manifest : [manifest]
+
+ if (playlist.length === 0) {
+ this.console.warn(`[PLAYER] Playlist is empty, aborting...`)
+ return false
+ }
+
+ if (playlist.some((item) => typeof item === "string")) {
+ playlist = await this.service_providers.resolveMany(playlist)
+ }
+
+ playlist = playlist.slice(startIndex)
+
+ for await (const [index, _manifest] of playlist.entries()) {
+ const instance = await this.createInstance(_manifest)
+
+ this.track_next_instances.push(instance)
+
+ if (index === 0) {
+ this.play(this.track_next_instances[0], {
+ time: time ?? 0
+ })
+ }
+ }
+
+ return manifest
+ }
+
+ next({ sync = false } = {}) {
+ if (this.state.control_locked && !sync) {
+ //this.console.warn("Sync mode is locked, cannot do this action")
+ return false
+ }
+
+ if (this.track_next_instances.length > 0) {
+ // move current audio instance to history
+ this.track_prev_instances.push(this.track_next_instances.shift())
+ }
+
+ if (this.track_next_instances.length === 0) {
+ this.console.log(`[PLAYER] No more tracks to play, stopping...`)
+
+ return this.stop()
+ }
+
+ let nextIndex = 0
+
+ if (this.state.playback_mode === "shuffle") {
+ nextIndex = Math.floor(Math.random() * this.track_next_instances.length)
+ }
+
+ this.play(this.track_next_instances[nextIndex])
+ }
+
+ previous({ sync = false } = {}) {
+ if (this.state.control_locked && !sync) {
+ //this.console.warn("Sync mode is locked, cannot do this action")
+ return false
+ }
+
+ if (this.track_prev_instances.length > 0) {
+ // move current audio instance to history
+ this.track_next_instances.unshift(this.track_prev_instances.pop())
+
+ return this.play(this.track_next_instances[0])
+ }
+
+ if (this.track_prev_instances.length === 0) {
+ this.console.log(`[PLAYER] No previous tracks, replying...`)
+ // replay the current track
+ return this.play(this.track_instance)
+ }
+ }
+
+ async togglePlayback() {
+ if (this.state.playback_status === "paused") {
+ await this.resumePlayback()
+ } else {
+ await this.pausePlayback()
+ }
+ }
+
+ async pausePlayback() {
+ return await new Promise((resolve, reject) => {
+ if (!this.track_instance) {
+ this.console.error("No audio instance")
+ return null
+ }
+
+ // set gain exponentially
+ this.track_instance.gainNode.gain.linearRampToValueAtTime(
+ 0.0001,
+ this.audioContext.currentTime + (Player.gradualFadeMs / 1000)
+ )
+
+ setTimeout(() => {
+ this.track_instance.media.pause()
+ resolve()
+ }, Player.gradualFadeMs)
+
+ this.native_controls.updateIsPlaying(false)
+ })
+ }
+
+ async resumePlayback() {
+ if (!this.state.playback_status === "playing") {
+ return true
+ }
+
+ return await new Promise((resolve, reject) => {
+ if (!this.track_instance) {
+ this.console.error("No audio instance")
+ return null
+ }
+
+ // ensure audio elemeto starts from 0 volume
+ this.track_instance.gainNode.gain.value = 0.0001
+
+ this.track_instance.media.play().then(() => {
+ resolve()
+ })
+
+ // set gain exponentially
+ this.track_instance.gainNode.gain.linearRampToValueAtTime(
+ this.state.volume,
+ this.audioContext.currentTime + (Player.gradualFadeMs / 1000)
+ )
+
+ this.native_controls.updateIsPlaying(true)
+ })
+ }
+
+ stop() {
+ this.destroyCurrentInstance()
+ this.abortPreloads()
+
+ this.state.playback_status = "stopped"
+ this.state.track_manifest = null
+
+ this.state.livestream_mode = false
+
+ this.track_instance = null
+ this.track_next_instances = []
+ this.track_prev_instances = []
+
+ this.native_controls.destroy()
+ }
+
+ mute(to) {
+ if (app.isMobile && typeof to !== "boolean") {
+ this.console.warn("Cannot mute on mobile")
+ return false
+ }
+
+ if (typeof to === "boolean") {
+ this.state.muted = to
+ this.track_instance.media.muted = to
+ }
+
+ return this.state.muted
+ }
+
+ volume(volume) {
+ if (typeof volume !== "number") {
+ return this.state.volume
+ }
+
+ if (app.isMobile) {
+ this.console.warn("Cannot change volume on mobile")
+ return false
+ }
+
+ if (volume > 1) {
+ if (!app.cores.settings.get("player.allowVolumeOver100")) {
+ volume = 1
+ }
+ }
+
+ if (volume < 0) {
+ volume = 0
+ }
+
+ this.state.volume = volume
+
+ AudioPlayerStorage.set("volume", volume)
+
+ if (this.track_instance) {
+ if (this.track_instance.gainNode) {
+ this.track_instance.gainNode.gain.value = this.state.volume
+ }
+ }
+
+ return this.state.volume
+ }
+
+ seek(time, { sync = false } = {}) {
+ if (!this.track_instance || !this.track_instance.media) {
+ return false
+ }
+
+ // if time not provided, return current time
+ if (typeof time === "undefined") {
+ return this.track_instance.media.currentTime
+ }
+
+ if (this.state.control_locked && !sync) {
+ this.console.warn("Sync mode is locked, cannot do this action")
+ return false
+ }
+
+
+ // if time is provided, seek to that time
+ if (typeof time === "number") {
+ this.console.log(`Seeking to ${time} | Duration: ${this.track_instance.media.duration}`)
+
+ this.track_instance.media.currentTime = time
+
+ return time
+ }
+ }
+
+ playbackMode(mode) {
+ if (typeof mode !== "string") {
+ return this.state.playback_mode
+ }
+
+ this.state.playback_mode = mode
+
+ if (this.track_instance) {
+ this.track_instance.media.loop = this.state.playback_mode === "repeat"
+ }
+
+ AudioPlayerStorage.set("mode", mode)
+
+ return mode
+ }
+
+ duration() {
+ if (!this.track_instance) {
+ return false
+ }
+
+ return this.track_instance.media.duration
+ }
+
+ loop(to) {
+ if (typeof to !== "boolean") {
+ this.console.warn("Loop must be a boolean")
+ return false
+ }
+
+ this.state.loop = to ?? !this.state.loop
+
+ if (this.track_instance.media) {
+ this.track_instance.media.loop = this.state.loop
+ }
+
+ return this.state.loop
+ }
+
+ close() {
+ this.stop()
+ this.detachPlayerComponent()
+ }
+
+ toggleMinimize(to) {
+ this.state.minimized = to ?? !this.state.minimized
+
+ if (this.state.minimized) {
+ app.layout.sidebar.attachBottomItem("player", BackgroundMediaPlayer, {
+ noContainer: true
+ })
+ } else {
+ app.layout.sidebar.removeBottomItem("player")
+ }
+
+ return this.state.minimized
+ }
+
+ toggleCollapse(to) {
+ if (typeof to !== "boolean") {
+ this.console.warn("Collapse must be a boolean")
+ return false
+ }
+
+ this.state.collapsed = to ?? !this.state.collapsed
+
+ return this.state.collapsed
+ }
+
+ toggleSyncMode(to, lock) {
+ if (typeof to !== "boolean") {
+ this.console.warn("Sync mode must be a boolean")
+ return false
+ }
+
+ this.state.syncMode = to ?? !this.state.syncMode
+
+ this.state.syncModeLocked = lock ?? false
+
+ this.console.log(`Sync mode is now ${this.state.syncMode ? "enabled" : "disabled"} | Locked: ${this.state.syncModeLocked ? "yes" : "no"}`)
+
+ return this.state.syncMode
+ }
+
+ toggleMute(to) {
+ if (typeof to !== "boolean") {
+ to = !this.state.muted
+ }
+
+ return this.mute(to)
+ }
+
+ async getTracksByIds(list) {
+ if (!Array.isArray(list)) {
+ this.console.warn("List must be an array")
+ return false
+ }
+
+ let ids = []
+
+ list.forEach((item) => {
+ if (typeof item === "string") {
+ ids.push(item)
+ }
+ })
+
+ if (ids.length === 0) {
+ return list
+ }
+
+ const fetchedTracks = await MusicModel.getTracksData(ids).catch((err) => {
+ this.console.error(err)
+ return false
+ })
+
+ if (!fetchedTracks) {
+ return list
+ }
+
+ // replace fetched tracks with the ones in the list
+ fetchedTracks.forEach((fetchedTrack) => {
+ const index = list.findIndex((item) => item === fetchedTrack._id)
+
+ if (index !== -1) {
+ list[index] = fetchedTrack
+ }
+ })
+
+ return list
+ }
+
+ async setSampleRate(to) {
+ // must be a integer
+ if (typeof to !== "number") {
+ this.console.error("Sample rate must be a number")
+ return this.audioContext.sampleRate
+ }
+
+ // must be a integer
+ if (!Number.isInteger(to)) {
+ this.console.error("Sample rate must be a integer")
+ return this.audioContext.sampleRate
+ }
+
+ return await new Promise((resolve, reject) => {
+ app.confirm({
+ title: "Change sample rate",
+ content: `To change the sample rate, the app needs to be reloaded. Do you want to continue?`,
+ onOk: () => {
+ try {
+ this.audioContext = new AudioContext({ sampleRate: to })
+
+ AudioPlayerStorage.set("sample_rate", to)
+
+ app.navigation.reload()
+
+ return resolve(this.audioContext.sampleRate)
+ } catch (error) {
+ app.message.error(`Failed to change sample rate, ${error.message}`)
+ return resolve(this.audioContext.sampleRate)
+ }
+ },
+ onCancel: () => {
+ return resolve(this.audioContext.sampleRate)
+ }
+ })
+ })
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/cores/player/player.core.js b/packages/app/src/cores/player/player.core.js
index 9eeb8115..5fe57d79 100755
--- a/packages/app/src/cores/player/player.core.js
+++ b/packages/app/src/cores/player/player.core.js
@@ -3,17 +3,16 @@ import EventEmitter from "evite/src/internals/EventEmitter"
import { Observable } from "object-observer"
import { FastAverageColor } from "fast-average-color"
-import MusicModel from "comty.js/models/music"
-
import ToolBarPlayer from "@components/Player/ToolBarPlayer"
import BackgroundMediaPlayer from "@components/Player/BackgroundMediaPlayer"
import AudioPlayerStorage from "./player.storage"
+import TrackInstanceClass from "./classes/TrackInstance"
import defaultAudioProccessors from "./processors"
import MediaSession from "./mediaSession"
-import ServicesHandlers from "./services"
+import ServiceProviders from "./services"
export default class Player extends Core {
static dependencies = [
@@ -33,6 +32,8 @@ export default class Player extends Core {
// buffer & precomputation
static maxManifestPrecompute = 3
+ service_providers = new ServiceProviders()
+
native_controls = new MediaSession()
currentDomWindow = null
@@ -94,7 +95,6 @@ export default class Player extends Core {
seek: this.seek.bind(this),
minimize: this.toggleMinimize.bind(this),
collapse: this.toggleCollapse.bind(this),
- toggleCurrentTrackLike: this.toggleCurrentTrackLike.bind(this),
state: new Proxy(this.state, {
get: (target, prop) => {
return target[prop]
@@ -112,6 +112,9 @@ export default class Player extends Core {
}
}),
gradualFadeMs: Player.gradualFadeMs,
+ trackInstance: () => {
+ return this.track_instance
+ }
}
internalEvents = {
@@ -181,8 +184,6 @@ export default class Player extends Core {
}
for await (const processor of this.audioProcessors) {
- this.console.log(`Initializing audio processor ${processor.constructor.name}`, processor)
-
if (typeof processor._init === "function") {
try {
await processor._init(this.audioContext)
@@ -277,7 +278,7 @@ export default class Player extends Core {
}
if (!instance._preloaded) {
- instance.media.preload = "metadata"
+ instance.audio.preload = "metadata"
instance._preloaded = true
}
@@ -294,8 +295,8 @@ export default class Player extends Core {
}
// stop playback
- if (this.track_instance.media) {
- this.track_instance.media.pause()
+ if (this.track_instance.audio) {
+ this.track_instance.audio.pause()
}
// reset track_instance
@@ -305,154 +306,10 @@ export default class Player extends Core {
this.state.livestream_mode = false
}
- async createInstance(manifest) {
- if (!manifest) {
- this.console.error("Manifest is required")
- return false
- }
-
- if (typeof manifest === "string") {
- manifest = {
- src: manifest,
- }
- }
-
- // check if manifest has `manifest` property, if is and not inherit or missing source, resolve
- if (manifest.service) {
- if (!ServicesHandlers[manifest.service]) {
- this.console.error(`Service ${manifest.service} is not supported`)
- return false
- }
-
- if (manifest.service !== "inherit" && !manifest.source) {
- const resolver = ServicesHandlers[manifest.service].resolve
-
- if (!resolver) {
- this.console.error(`Resolving for service [${manifest.service}] is not supported`)
- return false
- }
-
- manifest = await resolver(manifest)
- }
- }
-
- if (!manifest.src && !manifest.source) {
- this.console.error("Manifest source is required")
- return false
- }
-
- const source = manifest.src ?? manifest.source
-
- if (!manifest.metadata) {
- manifest.metadata = {}
- }
-
- // if title is not set, use the audio source filename
- if (!manifest.metadata.title) {
- manifest.metadata.title = source.split("/").pop()
- }
-
- let instance = {
- manifest: manifest,
- attachedProcessors: [],
- abortController: new AbortController(),
- source: source,
- media: new Audio(source),
- duration: null,
- seek: 0,
- track: null,
- }
-
- instance.media.signal = instance.abortController.signal
- instance.media.crossOrigin = "anonymous"
- instance.media.preload = "none"
-
- instance.media.loop = this.state.playback_mode === "repeat"
- instance.media.volume = this.state.volume
-
- // handle on end
- instance.media.addEventListener("ended", () => {
- this.next()
- })
-
- instance.media.addEventListener("loadeddata", () => {
- this.state.loading = false
- })
-
- // update playback status
- instance.media.addEventListener("play", () => {
- this.state.playback_status = "playing"
- })
-
- instance.media.addEventListener("playing", () => {
- this.state.loading = false
-
- this.state.playback_status = "playing"
-
- if (this.waitUpdateTimeout) {
- clearTimeout(this.waitUpdateTimeout)
- this.waitUpdateTimeout = null
- }
- })
-
- instance.media.addEventListener("pause", () => {
- this.state.playback_status = "paused"
- })
-
- instance.media.addEventListener("durationchange", (duration) => {
- if (instance.media.paused) {
- return false
- }
-
- instance.duration = duration
- })
-
- instance.media.addEventListener("waiting", () => {
- if (instance.media.paused) {
- return false
- }
-
- if (this.waitUpdateTimeout) {
- clearTimeout(this.waitUpdateTimeout)
- this.waitUpdateTimeout = null
- }
-
- // if takes more than 150ms to load, update loading state
- this.waitUpdateTimeout = setTimeout(() => {
- this.state.loading = true
- }, 150)
- })
-
- instance.media.addEventListener("seeked", () => {
- instance.seek = instance.media.currentTime
-
- if (this.state.sync_mode) {
- // useMusicSync("music:player:seek", {
- // position: instance.seek,
- // state: this.state,
- // })
- }
-
- this.eventBus.emit(`player.seeked`, instance.seek)
- })
-
- instance.media.addEventListener("loadedmetadata", () => {
- if (instance.media.duration === Infinity) {
- instance.manifest.stream = true
-
- this.state.livestream_mode = true
- }
- }, { once: true })
-
- instance.track = this.audioContext.createMediaElementSource(instance.media)
-
- return instance
- }
-
async attachProcessorsToInstance(instance) {
for await (const [index, processor] of this.audioProcessors.entries()) {
if (processor.constructor.node_bypass === true) {
- instance.track.connect(processor.processor)
+ instance.contextElement.connect(processor.processor)
processor.processor.connect(this.audioContext.destination)
@@ -515,35 +372,32 @@ export default class Player extends Core {
this.track_instance = await this.preloadAudioInstance(instance)
// reconstruct audio src if is not set
- if (this.track_instance.media.src !== instance.source) {
- this.track_instance.media.src = instance.source
+ if (this.track_instance.audio.src !== instance.manifest.source) {
+ this.track_instance.audio.src = instance.manifest.source
}
// set time to 0
- this.track_instance.media.currentTime = 0
+ this.track_instance.audio.currentTime = 0
if (params.time >= 0) {
- this.track_instance.media.currentTime = params.time
+ this.track_instance.audio.currentTime = params.time
}
- if (params.volume >= 0) {
- this.track_instance.gainNode.gain.value = params.volume
- } else {
- this.track_instance.gainNode.gain.value = this.state.volume
- }
-
- this.track_instance.media.muted = this.state.muted
- this.track_instance.media.loop = this.state.playback_mode === "repeat"
+ this.track_instance.audio.muted = this.state.muted
+ this.track_instance.audio.loop = this.state.playback_mode === "repeat"
+
+ this.track_instance.gainNode.gain.value = this.state.volume
// try to preload next audio
+ // TODO: Use a better way to preload queues
if (this.track_next_instances.length > 0) {
this.preloadAudioInstance(1)
}
// play
- await this.track_instance.media.play()
+ await this.track_instance.audio.play()
- this.console.log(this.track_instance)
+ this.console.debug(`Playing track >`, this.track_instance)
// update manifest
this.state.track_manifest = instance.manifest
@@ -572,45 +426,31 @@ export default class Player extends Core {
this.track_prev_instances = []
this.track_next_instances = []
- const isPlaylist = Array.isArray(manifest)
+ let playlist = Array.isArray(manifest) ? manifest : [manifest]
- if (isPlaylist) {
- let playlist = manifest
-
- if (playlist.length === 0) {
- this.console.warn(`[PLAYER] Playlist is empty, aborting...`)
- return false
- }
-
- if (playlist.some((item) => typeof item === "string")) {
- this.console.log("Resolving missing manifests by ids...")
- playlist = await ServicesHandlers.default.resolveMany(playlist)
- }
-
- playlist = playlist.slice(startIndex)
-
- for await (const [index, _manifest] of playlist.entries()) {
- const instance = await this.createInstance(_manifest)
-
- this.track_next_instances.push(instance)
-
- if (index === 0) {
- this.play(this.track_next_instances[0], {
- time: time ?? 0
- })
- }
- }
-
- return playlist
+ if (playlist.length === 0) {
+ this.console.warn(`Playlist is empty, aborting...`)
+ return false
}
- const instance = await this.createInstance(manifest)
+ if (playlist.some((item) => typeof item === "string")) {
+ playlist = await this.service_providers.resolveMany(playlist)
+ }
- this.track_next_instances.push(instance)
+ playlist = playlist.slice(startIndex)
- this.play(this.track_next_instances[0], {
- time: time ?? 0
- })
+ for await (const [index, _manifest] of playlist.entries()) {
+ let instance = new TrackInstanceClass(this, _manifest)
+ instance = await instance.initialize()
+
+ this.track_next_instances.push(instance)
+
+ if (index === 0) {
+ this.play(this.track_next_instances[0], {
+ time: time ?? 0
+ })
+ }
+ }
return manifest
}
@@ -627,7 +467,7 @@ export default class Player extends Core {
}
if (this.track_next_instances.length === 0) {
- this.console.log(`[PLAYER] No more tracks to play, stopping...`)
+ this.console.log(`No more tracks to play, stopping...`)
return this.stop()
}
@@ -683,7 +523,7 @@ export default class Player extends Core {
)
setTimeout(() => {
- this.track_instance.media.pause()
+ this.track_instance.audio.pause()
resolve()
}, Player.gradualFadeMs)
@@ -705,7 +545,7 @@ export default class Player extends Core {
// ensure audio elemeto starts from 0 volume
this.track_instance.gainNode.gain.value = 0.0001
- this.track_instance.media.play().then(() => {
+ this.track_instance.audio.play().then(() => {
resolve()
})
@@ -743,7 +583,7 @@ export default class Player extends Core {
if (typeof to === "boolean") {
this.state.muted = to
- this.track_instance.media.muted = to
+ this.track_instance.audio.muted = to
}
return this.state.muted
@@ -783,13 +623,13 @@ export default class Player extends Core {
}
seek(time, { sync = false } = {}) {
- if (!this.track_instance || !this.track_instance.media) {
+ if (!this.track_instance || !this.track_instance.audio) {
return false
}
// if time not provided, return current time
if (typeof time === "undefined") {
- return this.track_instance.media.currentTime
+ return this.track_instance.audio.currentTime
}
if (this.state.control_locked && !sync) {
@@ -797,9 +637,12 @@ export default class Player extends Core {
return false
}
+
// if time is provided, seek to that time
if (typeof time === "number") {
- this.track_instance.media.currentTime = time
+ this.console.log(`Seeking to ${time} | Duration: ${this.track_instance.audio.duration}`)
+
+ this.track_instance.audio.currentTime = time
return time
}
@@ -813,7 +656,7 @@ export default class Player extends Core {
this.state.playback_mode = mode
if (this.track_instance) {
- this.track_instance.media.loop = this.state.playback_mode === "repeat"
+ this.track_instance.audio.loop = this.state.playback_mode === "repeat"
}
AudioPlayerStorage.set("mode", mode)
@@ -826,7 +669,7 @@ export default class Player extends Core {
return false
}
- return this.track_instance.media.duration
+ return this.track_instance.audio.duration
}
loop(to) {
@@ -837,8 +680,8 @@ export default class Player extends Core {
this.state.loop = to ?? !this.state.loop
- if (this.track_instance.media) {
- this.track_instance.media.loop = this.state.loop
+ if (this.track_instance.audio) {
+ this.track_instance.audio.loop = this.state.loop
}
return this.state.loop
@@ -897,45 +740,6 @@ export default class Player extends Core {
return this.mute(to)
}
- async getTracksByIds(list) {
- if (!Array.isArray(list)) {
- this.console.warn("List must be an array")
- return false
- }
-
- let ids = []
-
- list.forEach((item) => {
- if (typeof item === "string") {
- ids.push(item)
- }
- })
-
- if (ids.length === 0) {
- return list
- }
-
- const fetchedTracks = await MusicModel.getTracksData(ids).catch((err) => {
- this.console.error(err)
- return false
- })
-
- if (!fetchedTracks) {
- return list
- }
-
- // replace fetched tracks with the ones in the list
- fetchedTracks.forEach((fetchedTrack) => {
- const index = list.findIndex((item) => item === fetchedTrack._id)
-
- if (index !== -1) {
- list[index] = fetchedTrack
- }
- })
-
- return list
- }
-
async setSampleRate(to) {
// must be a integer
if (typeof to !== "number") {
@@ -973,38 +777,4 @@ export default class Player extends Core {
})
})
}
-
- async toggleCurrentTrackLike(to, manifest) {
- let isCurrent = !!!manifest
-
- if (typeof manifest === "undefined") {
- manifest = this.track_instance.manifest
- }
-
- if (!manifest) {
- this.console.error("Track instance or manifest not found")
- return false
- }
-
- if (typeof to !== "boolean") {
- this.console.warn("Like must be a boolean")
- return false
- }
-
- const service = manifest.service ?? "default"
-
- if (!ServicesHandlers[service].toggleLike) {
- this.console.error(`Service [${service}] does not support like actions`)
- return false
- }
-
- const result = await ServicesHandlers[service].toggleLike(manifest, to)
-
- if (isCurrent) {
- this.track_instance.manifest.liked = to
- this.state.track_manifest.liked = to
- }
-
- return result
- }
}
\ No newline at end of file
diff --git a/packages/app/src/cores/player/processors/node.js b/packages/app/src/cores/player/processors/node.js
index 1fd83628..84a6a865 100755
--- a/packages/app/src/cores/player/processors/node.js
+++ b/packages/app/src/cores/player/processors/node.js
@@ -72,7 +72,7 @@ export default class ProcessorNode {
prevNode.processor._last.connect(this.processor)
} else {
//console.log(`[${this.constructor.refName ?? this.constructor.name}] node is the first node, connecting to the media source...`)
- instance.track.connect(this.processor)
+ instance.contextElement.connect(this.processor)
}
// now, check if it has a next node
diff --git a/packages/app/src/cores/player/services.js b/packages/app/src/cores/player/services.js
index 363a998c..32508f5e 100755
--- a/packages/app/src/cores/player/services.js
+++ b/packages/app/src/cores/player/services.js
@@ -1,21 +1,59 @@
import MusicModel from "comty.js/models/music"
-export default {
- "default": {
- resolve: async (track_id) => {
- return await MusicModel.getTrackData(track_id)
- },
- resolveMany: async (track_ids, options) => {
- const response = await MusicModel.getTrackData(track_ids, options)
+class ComtyMusicService {
+ static id = "default"
- if (response.list) {
- return response
- }
+ resolve = async (track_id) => {
+ return await MusicModel.getTrackData(track_id)
+ }
- return [response]
- },
- toggleLike: async (manifest, to) => {
- return await MusicModel.toggleTrackLike(manifest, to)
+ resolveMany = async (track_ids, options) => {
+ const response = await MusicModel.getTrackData(track_ids, options)
+
+ if (response.list) {
+ return response
}
+
+ return [response]
+ }
+
+ toggleTrackLike = async (manifest, to) => {
+ return await MusicModel.toggleTrackLike(manifest, to)
+ }
+}
+
+export default class ServiceProviders {
+ providers = [
+ new ComtyMusicService()
+ ]
+
+ findProvider(providerId) {
+ return this.providers.find((provider) => provider.constructor.id === providerId)
+ }
+
+ register(provider) {
+ this.providers.push(provider)
+ }
+
+ // operations
+ resolve = async (providerId, manifest) => {
+ const provider = await this.findProvider(providerId)
+
+ if (!provider) {
+ console.error(`Failed to resolve manifest, provider [${providerId}] not registered`)
+ return manifest
+ }
+
+ return await provider.resolve(manifest)
+ }
+
+ resolveMany = async (manifests) => {
+ manifests = manifests.map(async (manifest) => {
+ return await this.resolve(manifest.service ?? "default", manifest)
+ })
+
+ manifests = await Promise.all(manifests)
+
+ return manifests
}
}
\ No newline at end of file
diff --git a/packages/app/src/cores/sfx/sfx.core.js b/packages/app/src/cores/sfx/sfx.core.js
index a0577e64..51841fc8 100755
--- a/packages/app/src/cores/sfx/sfx.core.js
+++ b/packages/app/src/cores/sfx/sfx.core.js
@@ -55,8 +55,6 @@ export default class SFXCore extends Core {
src: [path],
})
}
-
- this.console.log(this.soundsPool)
}
async play(name, options = {}) {
diff --git a/packages/app/src/cores/style/style.core.jsx b/packages/app/src/cores/style/style.core.jsx
index 12096da6..47b594f6 100755
--- a/packages/app/src/cores/style/style.core.jsx
+++ b/packages/app/src/cores/style/style.core.jsx
@@ -11,22 +11,32 @@ const variantToAlgorithm = {
dark: theme.darkAlgorithm,
}
+const ClientPrefersDark = () => window.matchMedia("(prefers-color-scheme: dark)")
+
+function variantKeyToColor(key) {
+ if (key == "auto") {
+ if (ClientPrefersDark().matches) {
+ return "dark"
+ }
+
+ return "light"
+ }
+
+ return key
+}
+
+
export class ThemeProvider extends React.Component {
state = {
- useAlgorigthm: app.cores.style.currentVariant ?? "dark",
- useCompactMode: app.cores.style.getValue("compact-mode"),
+ useAlgorigthm: variantKeyToColor(app.cores.style.currentVariantKey),
+ useCompactMode: app.cores.style.getVar("compact-mode"),
}
handleUpdate = (update) => {
console.log("[THEME] Update", update)
- if (update.themeVariant) {
- this.setState({
- useAlgorigthm: update.themeVariant
- })
- }
-
this.setState({
+ useAlgorigthm: variantKeyToColor(app.cores.style.currentVariantKey),
useCompactMode: update["compact-mode"]
})
}
@@ -51,7 +61,7 @@ export class ThemeProvider extends React.Component {
return
this.getVar(...args),
+ getDefaultVar: (...args) => this.getDefaultVar(...args),
+ getStoragedVariantKey: () => StyleCore.storagedVariantKey,
- return StyleCore.storagedVariant
+ applyStyles: (...args) => this.applyStyles(...args),
+ applyVariant: (...args) => this.applyVariant(...args),
+ applyTemporalVariant: (...args) => this.applyTemporalVariant(...args),
+
+ mutateTheme: (...args) => this.mutateTheme(...args),
+ resetToDefault: () => this.resetToDefault(),
+ toggleCompactMode: () => this.toggleCompactMode(),
}
async onInitialize() {
- if (StyleCore.storagedTheme) {
- // TODO: Start remote theme loader
- } else {
- this.public.theme = config.defaultTheme
- }
+ this.public.theme = config.defaultTheme
const modifications = StyleCore.storagedModifications
// override with static vars
if (this.public.theme.defaultVars) {
- this.update(this.public.theme.defaultVars)
+ this.applyStyles(this.public.theme.defaultVars)
}
// override theme with modifications
if (modifications) {
- this.update(modifications)
+ this.applyStyles(modifications)
}
// apply variation
- this.applyVariant(StyleCore.variant)
-
- // handle auto prefered color scheme
- window.matchMedia("(prefers-color-scheme: light)").addListener(() => {
- this.console.log(`[THEME] Auto color scheme changed`)
-
- this.applyVariant(StyleCore.variant)
- })
+ this.applyVariant(StyleCore.storagedVariantKey ?? StyleCore.defaultVariantKey)
// if mobile set fontScale to 1
if (app.isMobile) {
- this.update({
+ this.applyStyles({
fontScale: 1
})
}
- }
- onEvents = {
- "style:auto_darkmode": (value) => {
- if (value === true) {
- return this.applyVariant(StyleCore.variant)
+ ClientPrefersDark().addEventListener("change", (event) => {
+ this.console.log("[PREFERS-DARK] Change >", event.matches)
+
+ if (this.isOnTemporalVariant) {
+ return false
}
- return this.applyVariant(StyleCore.variant)
- }
+ if (event.matches) {
+ this.applyVariant("dark")
+ } else {
+ this.applyVariant("light")
+ }
+ })
}
- public = {
- theme: null,
- mutation: null,
- currentVariant: "dark",
-
- getValue: (...args) => this.getValue(...args),
- setDefault: () => this.setDefault(),
- update: (...args) => this.update(...args),
- applyVariant: (...args) => this.applyVariant(...args),
- applyInitialVariant: () => this.applyVariant(StyleCore.variant),
- compactMode: (value = !window.app.cores.settings.get("style.compactMode")) => {
- if (value) {
- return this.update({
- layoutMargin: 0,
- layoutPadding: 0,
- })
- }
-
- return this.update({
- layoutMargin: this.getValue("layoutMargin"),
- layoutPadding: this.getValue("layoutPadding"),
- })
- },
- modify: (value) => {
- this.public.update(value)
-
- this.applyVariant(this.public.mutation.themeVariant ?? this.public.currentVariant)
-
- StyleCore.storagedModifications = this.public.mutation
- },
- defaultVar: (key) => {
- if (!key) {
- return this.public.theme.defaultVars
- }
-
- return this.public.theme.defaultVars[key]
- },
- storagedVariant: StyleCore.storagedVariant,
- storagedModifications: StyleCore.storagedModifications,
- }
-
- getValue(key) {
+ getVar(key) {
if (typeof key === "undefined") {
return {
...this.public.theme.defaultVars,
- ...StyleCore.storagedModifications
+ ...StyleCore.storagedModifications,
}
}
return StyleCore.storagedModifications[key] || this.public.theme.defaultVars[key]
}
- setDefault() {
- store.remove(StyleCore.themeManifestStorageKey)
- store.remove(StyleCore.modificationStorageKey)
+ getDefaultVar(key) {
+ if (!key) {
+ return this.public.theme.defaultVars
+ }
- app.cores.settings.set("colorPrimary", this.public.theme.defaultVars.colorPrimary)
-
- this.onInitialize()
+ return this.public.theme.defaultVars[key]
}
- update(update) {
+ applyStyles(update) {
if (typeof update !== "object") {
+ this.console.error("Invalid update, must be an object")
return false
}
@@ -241,18 +214,62 @@ export default class StyleCore extends Core {
})
}
- applyVariant(variant = (this.public.theme.defaultVariant ?? "light")) {
- const values = this.public.theme.variants[variant]
+ applyVariant = (variantKey = (this.public.theme.defaultVariant ?? "light"), save = true) => {
+ if (save) {
+ StyleCore.storagedVariantKey = variantKey
+ this.public.currentVariantKey = variantKey
+ }
+
+ this.isOnTemporalVariant = false
+
+ this.console.log(`Input variant key [${variantKey}]`)
+
+ const color = variantKeyToColor(variantKey)
+
+ this.console.log(`Applying variant [${color}]`)
+
+ const values = this.public.theme.variants[color]
if (!values) {
- this.console.error(`Variant [${variant}] not found`)
+ this.console.error(`Variant [${color}] not found`)
return false
}
- values.themeVariant = variant
+ this.applyStyles(values)
+ }
- this.public.currentVariant = variant
+ applyTemporalVariant = (variantKey) => {
+ this.applyVariant(variantKey, false)
- this.update(values)
+ this.isOnTemporalVariant = true
+ }
+
+ mutateTheme(update) {
+ this.applyStyles(update)
+ this.applyVariant(this.public.currentVariantKey)
+
+ StyleCore.storagedModifications = this.public.mutation
+ }
+
+ toggleCompactMode(value = !window.app.cores.settings.get("style.compactMode")) {
+ if (value === true) {
+ return this.applyStyles({
+ layoutMargin: 0,
+ layoutPadding: 0,
+ })
+ }
+
+ return this.applyStyles({
+ layoutMargin: this.getVar("layoutMargin"),
+ layoutPadding: this.getVar("layoutPadding"),
+ })
+ }
+
+ resetToDefault() {
+ store.remove(StyleCore.modificationStorageKey)
+
+ app.cores.settings.set("colorPrimary", this.public.theme.defaultVars.colorPrimary)
+
+ this.onInitialize()
}
}
\ No newline at end of file
diff --git a/packages/app/src/hooks/useChat/index.js b/packages/app/src/hooks/useChat/index.js
index a19d4139..8cb12cd3 100644
--- a/packages/app/src/hooks/useChat/index.js
+++ b/packages/app/src/hooks/useChat/index.js
@@ -9,7 +9,7 @@ export default (to_user_id) => {
const [isRemoteTyping, setIsRemoteTyping] = React.useState(false)
const [timeoutOffTypingEvent, setTimeoutOffTypingEvent] = React.useState(null)
-
+
async function sendMessage(message) {
emitTypingEvent(false)
diff --git a/packages/app/src/hooks/useClickNavById/index.js b/packages/app/src/hooks/useClickNavById/index.js
new file mode 100644
index 00000000..b122a6f6
--- /dev/null
+++ b/packages/app/src/hooks/useClickNavById/index.js
@@ -0,0 +1,39 @@
+import React from "react"
+
+const useClickNavById = (navigators = {}, itemFlagId = "div") => {
+ const ref = React.useRef(null)
+
+ async function onClick(e) {
+ const element = e.target.closest(itemFlagId ?? "div")
+
+ if (!element) {
+ console.error("Element not found")
+ return false
+ }
+
+ const id = element?.id
+
+ if (!id) {
+ console.error("Element id not found")
+ return false
+ }
+
+ const location = navigators[id]
+
+ if (!location) {
+ console.error("Location not found")
+ return false
+ }
+
+ app.location.push(location)
+ }
+
+ return [
+ ref,
+ {
+ onClick
+ }
+ ]
+}
+
+export default useClickNavById
\ No newline at end of file
diff --git a/packages/app/src/hooks/useHideOnMouseStop/index.jsx b/packages/app/src/hooks/useHideOnMouseStop/index.jsx
new file mode 100644
index 00000000..ef240468
--- /dev/null
+++ b/packages/app/src/hooks/useHideOnMouseStop/index.jsx
@@ -0,0 +1,59 @@
+import React from "react"
+
+let timer = null
+
+const useHideOnMouseStop = ({
+ delay = 2000,
+ hideCursor = false,
+ initialHide = false,
+ showOnlyOnContainerHover = false,
+}) => {
+ const [hide, setHide] = React.useState(initialHide)
+ const mountedRef = React.useRef(false)
+ const [hover, setHover] = React.useState(false)
+ const toggleVisibility = React.useCallback((hide, cursor) => {
+ setHide(hide)
+ if (hideCursor) {
+ document.body.style.cursor = cursor
+ }
+ }, [hideCursor])
+ const onMouseEnter = React.useCallback(() => setHover(true), [setHover])
+ const onMouseLeave = React.useCallback(() => setHover(false), [setHover])
+ const onMouseMove = React.useCallback(() => {
+ clearTimeout(timer)
+
+ if (hide && mountedRef.current) {
+ if (showOnlyOnContainerHover && hover) {
+ toggleVisibility(!hide, "default")
+ } else if (!showOnlyOnContainerHover) {
+ toggleVisibility(!hide, "default")
+ }
+ }
+
+ timer = setTimeout(() => {
+ if (!hover && mountedRef.current) {
+ toggleVisibility(true, "none")
+ }
+ }, delay)
+ }, [hide, hover, setHide])
+
+ React.useEffect(() => {
+ mountedRef.current = true
+
+ return () => {
+ mountedRef.current = false
+ }
+ }, [])
+
+ React.useEffect(() => {
+ window.addEventListener("mousemove", onMouseMove)
+
+ return () => {
+ window.removeEventListener("mousemove", onMouseMove)
+ }
+ }, [onMouseMove])
+
+ return [hide, onMouseEnter, onMouseLeave]
+}
+
+export default useHideOnMouseStop
\ No newline at end of file
diff --git a/packages/app/src/utils/useMaxScreen/index.js b/packages/app/src/hooks/useMaxScreen/index.js
similarity index 60%
rename from packages/app/src/utils/useMaxScreen/index.js
rename to packages/app/src/hooks/useMaxScreen/index.js
index 0ff14951..5c2dae74 100644
--- a/packages/app/src/utils/useMaxScreen/index.js
+++ b/packages/app/src/hooks/useMaxScreen/index.js
@@ -2,15 +2,16 @@ import React from "react"
export default () => {
const enterPlayerAnimation = () => {
- app.cores.style.applyVariant("dark")
- app.cores.style.compactMode(true)
+ app.cores.style.applyTemporalVariant("dark")
+ app.cores.style.toggleCompactMode(true)
app.layout.toggleCenteredContent(false)
app.controls.toggleUIVisibility(false)
}
const exitPlayerAnimation = () => {
- app.cores.style.applyInitialVariant()
- app.cores.style.compactMode(false)
+ app.cores.style.applyVariant(app.cores.style.getStoragedVariantKey())
+ app.cores.style.toggleCompactMode(false)
+ app.layout.toggleCenteredContent(true)
app.controls.toggleUIVisibility(true)
}
diff --git a/packages/app/src/layouts/components/bottomBar/index.jsx b/packages/app/src/layouts/components/bottomBar/index.jsx
index eff618e9..8108dad1 100755
--- a/packages/app/src/layouts/components/bottomBar/index.jsx
+++ b/packages/app/src/layouts/components/bottomBar/index.jsx
@@ -31,10 +31,10 @@ const tourSteps = [
]
const openPlayerView = () => {
- app.DrawerController.open("player", PlayerView)
+ app.layout.drawer.open("player", PlayerView)
}
const openCreator = () => {
- app.DrawerController.open("creator", CreatorView, {
+ app.layout.drawer.open("creator", CreatorView, {
props: {
bodyStyle: {
minHeight: "unset",
@@ -336,7 +336,7 @@ export class BottomBar extends React.Component {
}
- const heightValue = this.state.visible ? Number(app.cores.style.defaultVar("bottom-bar-height").replace("px", "")) : 0
+ const heightValue = this.state.visible ? Number(app.cores.style.getDefaultVar("bottom-bar-height").replace("px", "")) : 0
return <>
{
diff --git a/packages/app/src/layouts/components/drawer/index.jsx b/packages/app/src/layouts/components/drawer/index.jsx
index b3747770..c351dbbb 100755
--- a/packages/app/src/layouts/components/drawer/index.jsx
+++ b/packages/app/src/layouts/components/drawer/index.jsx
@@ -13,7 +13,7 @@ export default class DrawerController extends React.Component {
drawers: [],
}
- window.app["DrawerController"] = {
+ app.layout.drawer = {
open: this.open,
close: this.close,
closeAll: this.closeAll,
diff --git a/packages/app/src/layouts/components/sidebar/index.jsx b/packages/app/src/layouts/components/sidebar/index.jsx
index 523dffb1..0bcdc791 100755
--- a/packages/app/src/layouts/components/sidebar/index.jsx
+++ b/packages/app/src/layouts/components/sidebar/index.jsx
@@ -11,17 +11,13 @@ import sidebarItems from "@config/sidebar"
import "./index.less"
-const extraItems = [
- {
- id: "insiders",
- title: "Insiders",
- icon: "MdToken",
- roles: ["insider"],
- path: "/insiders",
- }
-]
-
const onClickHandlers = {
+ addons: () => {
+ window.app.location.push("/addons")
+ },
+ studio: () => {
+ window.app.location.push("/studio")
+ },
settings: () => {
window.app.navigation.goToSettings()
},
@@ -99,11 +95,29 @@ const BottomMenuDefaultItems = [
const ActionMenuItems = [
{
- key: "account",
+ key: "profile",
label: <>
- {t => t("Account")}
+ {t => t("Profile")}
+
+ >,
+ },
+ {
+ key: "studio",
+ label: <>
+
+
+ {t => t("Studio")}
+
+ >,
+ },
+ {
+ key: "addons",
+ label: <>
+
+
+ {t => t("Addons")}
>,
},
@@ -256,8 +270,6 @@ export default class Sidebar extends React.Component {
}
componentDidMount = async () => {
- this.computeExtraItems()
-
for (const [event, handler] of Object.entries(this.events)) {
app.eventBus.on(event, handler)
}
@@ -279,28 +291,6 @@ export default class Sidebar extends React.Component {
//delete app.layout.sidebar
}
- computeExtraItems = async () => {
- const roles = await app.cores.permissions.getRoles()
-
- const resultItems = []
-
- if (roles.includes("admin")) {
- resultItems.push(...extraItems)
- } else {
- extraItems.forEach((item) => {
- item.roles.every((role) => {
- if (roles.includes(role)) {
- resultItems.push(item)
- }
- })
- })
- }
-
- this.setState({
- topItems: generateTopItems(resultItems)
- })
- }
-
handleClick = (e) => {
if (e.item.props.ignore_click === "true") {
return
@@ -470,7 +460,6 @@ export default class Sidebar extends React.Component {
mode="inline"
onClick={this.handleClick}
items={this.getBottomItems()}
-
/>
diff --git a/packages/app/src/layouts/components/toolsBar/index.jsx b/packages/app/src/layouts/components/toolsBar/index.jsx
index f222180e..ee9c8270 100755
--- a/packages/app/src/layouts/components/toolsBar/index.jsx
+++ b/packages/app/src/layouts/components/toolsBar/index.jsx
@@ -55,26 +55,28 @@ export default class ToolsBar extends React.Component {
}
render() {
- return
+ return
{({ x, width }) => {
return