mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 02:24:16 +00:00
merge from local
This commit is contained in:
parent
d041729e3e
commit
a373a27f5a
21
.vscode/settings.json
vendored
21
.vscode/settings.json
vendored
@ -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"
|
||||
}
|
||||
}
|
2
comty.js
2
comty.js
@ -1 +1 @@
|
||||
Subproject commit 126bad9c1e21c0c7fcab60a22fc9d70bbbd9a999
|
||||
Subproject commit 15f89af37c5bac086e8e1154f5d1b3da8967678b
|
@ -1 +1 @@
|
||||
Subproject commit 6d553830ab4661ffab952253d77ccb0bfc1363d8
|
||||
Subproject commit d2e6f1bc5856e3084d4fd068dec5d67ab2ef9d8d
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"app:language": "en",
|
||||
"app:language": "en_US",
|
||||
"low_performance_mode": false,
|
||||
"transcode_video_browser": false,
|
||||
"forceMobileMode": false,
|
||||
|
@ -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",
|
||||
}
|
||||
}
|
611
packages/app/config/languages.json
Normal file
611
packages/app/config/languages.json
Normal file
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
@ -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": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"build": {
|
||||
"devPath": "http://fr01.ragestudio.net:8000",
|
||||
"devPath": "https://fr01.ragestudio.net:8000",
|
||||
"distDir": "../dist"
|
||||
},
|
||||
"package": {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
58
packages/app/src/components/CoverEditor/index.jsx
Normal file
58
packages/app/src/components/CoverEditor/index.jsx
Normal file
@ -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 <div className="cover-editor">
|
||||
<div className="cover-editor-preview">
|
||||
<Image
|
||||
src={url}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="cover-editor-actions">
|
||||
<UploadButton
|
||||
onSuccess={(uid, response) => {
|
||||
setUrl(response.url)
|
||||
}}
|
||||
/>
|
||||
|
||||
<antd.Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setUrl(defaultUrl)
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</antd.Button>
|
||||
|
||||
{
|
||||
props.extraActions
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default CoverEditor
|
45
packages/app/src/components/CoverEditor/index.less
Normal file
45
packages/app/src/components/CoverEditor/index.less
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
<div className="field" onClick={this.onClickForgotPassword}>
|
||||
<a>Forgot your password?</a>
|
||||
</div>
|
||||
|
||||
<div className="field" onClick={this.onClickRegister}>
|
||||
<a>You need a account?</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@ -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) {
|
||||
|
116
packages/app/src/components/MusicStudio/LyricsEditor/index.jsx
Normal file
116
packages/app/src/components/MusicStudio/LyricsEditor/index.jsx
Normal file
@ -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 <div className="lyrics-editor">
|
||||
<h1>Lyrics</h1>
|
||||
|
||||
<antd.Select
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select a language"
|
||||
value={selectedLang}
|
||||
options={[...LanguagesMap, {
|
||||
label: "Original",
|
||||
value: "original",
|
||||
}]}
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) => (option?.label.toLowerCase() ?? '').includes(input.toLowerCase())}
|
||||
filterSort={(optionA, optionB) =>
|
||||
(optionA?.label.toLowerCase() ?? '').toLowerCase().localeCompare((optionB?.label.toLowerCase() ?? '').toLowerCase())
|
||||
}
|
||||
onChange={setSelectedLang}
|
||||
/>
|
||||
|
||||
<span>
|
||||
{selectedLang}
|
||||
</span>
|
||||
|
||||
{
|
||||
selectedLang && <UploadButton
|
||||
onSuccess={onUploadLRC}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
currentLangData && currentLangData?.value && <LyricsTextView
|
||||
track={props.track}
|
||||
lang={currentLangData}
|
||||
/>
|
||||
}
|
||||
{
|
||||
!currentLangData || !currentLangData?.value && <antd.Empty
|
||||
description="No lyrics available"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default LyricsEditor
|
@ -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 <antd.Result
|
||||
status="warning"
|
||||
title="Failed"
|
||||
subTitle={error.message}
|
||||
/>
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
return <div>
|
||||
<p>{lyrics}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default LyricsTextView
|
@ -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 <div className="music-studio-page-content">
|
||||
<div className="music-studio-page-header">
|
||||
<h1>Your Releases</h1>
|
||||
</div>
|
||||
|
||||
{
|
||||
L_MyReleases && !E_MyReleases && <antd.Skeleton active />
|
||||
}
|
||||
{
|
||||
E_MyReleases && <antd.Result
|
||||
status="warning"
|
||||
title="Failed to retrieve releases"
|
||||
subTitle={E_MyReleases.message}
|
||||
/>
|
||||
}
|
||||
{
|
||||
!L_MyReleases && !E_MyReleases && R_MyReleases && R_MyReleases.items.length === 0 && <antd.Empty />
|
||||
}
|
||||
|
||||
{
|
||||
!L_MyReleases && !E_MyReleases && R_MyReleases && R_MyReleases.items.length > 0 && <div className="music-studio-page-releases-list">
|
||||
{
|
||||
R_MyReleases.items.map((item) => {
|
||||
return <ReleaseItem
|
||||
key={item._id}
|
||||
release={item}
|
||||
onClick={onClickReleaseItem}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default MyReleasesList
|
@ -1,8 +1,8 @@
|
||||
.music-dashboard {
|
||||
.music-studio-page-releases-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
.music-dashboard_header {}
|
||||
gap: 10px;
|
||||
}
|
107
packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx
Normal file
107
packages/app/src/components/MusicStudio/ReleaseEditor/index.jsx
Normal file
@ -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 <antd.Result
|
||||
status="warning"
|
||||
title="Error"
|
||||
subTitle={E_Release.message}
|
||||
/>
|
||||
}
|
||||
|
||||
if (L_Release) {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
const Tab = Tabs.find(({ key }) => key === selectedTab)
|
||||
|
||||
return <div className="music-studio-release-editor">
|
||||
<div className="music-studio-release-editor-menu">
|
||||
<antd.Menu
|
||||
onClick={(e) => setSelectedTab(e.key)}
|
||||
selectedKeys={[selectedTab]}
|
||||
items={Tabs}
|
||||
mode="vertical"
|
||||
/>
|
||||
|
||||
<div className="music-studio-release-editor-menu-actions">
|
||||
<antd.Button
|
||||
type="primary"
|
||||
onClick={handleSubmit}
|
||||
icon={<Icons.Save />}
|
||||
disabled={L_Release || !canFinish()}
|
||||
>
|
||||
Save
|
||||
</antd.Button>
|
||||
|
||||
{
|
||||
release_id !== "new" ? <antd.Button
|
||||
icon={<Icons.IoMdTrash />}
|
||||
disabled={L_Release}
|
||||
>
|
||||
Delete
|
||||
</antd.Button> : null
|
||||
}
|
||||
|
||||
{
|
||||
release_id !== "new" ? <antd.Button
|
||||
icon={<Icons.MdLink />}
|
||||
onClick={() => app.location.push(`/music/release/${R_Release._id}`)}
|
||||
>
|
||||
Go to release
|
||||
</antd.Button> : null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="music-studio-release-editor-content">
|
||||
{
|
||||
!Tab && <antd.Result
|
||||
status="error"
|
||||
title="Error"
|
||||
subTitle="Tab not found"
|
||||
/>
|
||||
}
|
||||
{
|
||||
Tab && React.createElement(Tab.render, {
|
||||
release: R_Release,
|
||||
onFinish: onFinish,
|
||||
|
||||
references: {
|
||||
basic: basicInfoRef
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default ReleaseEditor
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import React from "react"
|
||||
|
||||
const ReleaseAdvanced = (props) => {
|
||||
return <div>
|
||||
<h1>Advanced</h1>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default ReleaseAdvanced
|
@ -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: <Icons.MdMusicNote />,
|
||||
},
|
||||
{
|
||||
value: "ep",
|
||||
label: "Episode",
|
||||
icon: <Icons.MdAlbum />,
|
||||
},
|
||||
{
|
||||
value: "album",
|
||||
label: "Album",
|
||||
icon: <Icons.MdAlbum />,
|
||||
},
|
||||
{
|
||||
value: "compilation",
|
||||
label: "Compilation",
|
||||
icon: <Icons.MdAlbum />,
|
||||
}
|
||||
]
|
||||
|
||||
const BasicInformation = (props) => {
|
||||
const { release, onFinish } = props
|
||||
|
||||
return <div className="music-studio-release-editor-tab">
|
||||
<h1>Release Information</h1>
|
||||
|
||||
<antd.Form
|
||||
name="basic"
|
||||
layout="vertical"
|
||||
ref={props.references.basic}
|
||||
onFinish={onFinish}
|
||||
requiredMark={false}
|
||||
>
|
||||
<antd.Form.Item
|
||||
label=""
|
||||
name="cover"
|
||||
rules={[{ required: true, message: "Input a cover for the release" }]}
|
||||
initialValue={release?.cover}
|
||||
>
|
||||
<CoverEditor
|
||||
defaultUrl="https://storage.ragestudio.net/comty-static-assets/default_song.png"
|
||||
/>
|
||||
</antd.Form.Item>
|
||||
|
||||
{
|
||||
release._id && <antd.Form.Item
|
||||
label={<><Icons.MdTag /> <span>ID</span></>}
|
||||
name="_id"
|
||||
initialValue={release._id}
|
||||
disabled
|
||||
>
|
||||
<antd.Input
|
||||
placeholder="Release ID"
|
||||
disabled
|
||||
/>
|
||||
</antd.Form.Item>
|
||||
}
|
||||
|
||||
<antd.Form.Item
|
||||
label={<><Icons.MdMusicNote /> <span>Title</span></>}
|
||||
name="title"
|
||||
rules={[{ required: true, message: "Input a title for the release" }]}
|
||||
initialValue={release?.title}
|
||||
>
|
||||
<antd.Input
|
||||
placeholder="Release title"
|
||||
maxLength={128}
|
||||
showCount
|
||||
/>
|
||||
</antd.Form.Item>
|
||||
|
||||
<antd.Form.Item
|
||||
label={<><Icons.MdAlbum /> <span>Type</span></>}
|
||||
name="type"
|
||||
rules={[{ required: true, message: "Select a type for the release" }]}
|
||||
initialValue={release?.type}
|
||||
>
|
||||
<antd.Select
|
||||
placeholder="Release type"
|
||||
options={ReleasesTypes}
|
||||
/>
|
||||
</antd.Form.Item>
|
||||
|
||||
<antd.Form.Item
|
||||
label={<><Icons.MdPublic /> <span>Public</span></>}
|
||||
name="public"
|
||||
initialValue={release?.public}
|
||||
>
|
||||
<antd.Switch />
|
||||
</antd.Form.Item>
|
||||
</antd.Form>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default BasicInformation
|
@ -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 <div className="uploadHint">
|
||||
<Icons.MdPlaylistAdd />
|
||||
<p>Upload your tracks</p>
|
||||
<p>Drag and drop your tracks here or click this box to start uploading files.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
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 <Draggable
|
||||
key={track._id}
|
||||
draggableId={track._id}
|
||||
index={props.index}
|
||||
>
|
||||
{
|
||||
(provided, snapshot) => {
|
||||
return <div
|
||||
className={classnames(
|
||||
"music-studio-release-editor-tracks-list-item",
|
||||
{
|
||||
["loading"]: loading,
|
||||
["failed"]: !!error
|
||||
}
|
||||
)}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
>
|
||||
<div className="music-studio-release-editor-tracks-list-item-index">
|
||||
<span>{props.index + 1}</span>
|
||||
</div>
|
||||
|
||||
<span>{track.title}</span>
|
||||
|
||||
<div className="music-studio-release-editor-tracks-list-item-actions">
|
||||
<antd.Button
|
||||
type="ghost"
|
||||
icon={<Icons.Edit2 />}
|
||||
onClick={onClickEditTrack}
|
||||
/>
|
||||
|
||||
<div
|
||||
{...provided.dragHandleProps}
|
||||
className="music-studio-release-editor-tracks-list-item-dragger"
|
||||
>
|
||||
<Icons.MdDragIndicator />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</Draggable>
|
||||
}
|
||||
|
||||
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 <div className="music-studio-release-editor-tab">
|
||||
<h1>Tracks</h1>
|
||||
|
||||
<div>
|
||||
<antd.Upload
|
||||
className="uploader"
|
||||
customRequest={handleUploadTrack}
|
||||
onChange={onTrackUploaderChange}
|
||||
showUploadList={false}
|
||||
accept="audio/*"
|
||||
multiple
|
||||
>
|
||||
{
|
||||
list.length === 0 ?
|
||||
<UploadHint /> : <antd.Button
|
||||
className="uploadMoreButton"
|
||||
icon={<Icons.Plus />}
|
||||
/>
|
||||
}
|
||||
</antd.Upload>
|
||||
|
||||
<DragDropContext
|
||||
onDragEnd={onTrackDragEnd}
|
||||
>
|
||||
<Droppable
|
||||
droppableId="droppable"
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
className="music-studio-release-editor-tracks-list"
|
||||
>
|
||||
{
|
||||
list.length === 0 && <antd.Result
|
||||
status="info"
|
||||
title="No tracks"
|
||||
/>
|
||||
}
|
||||
{
|
||||
list.map((track, index) => {
|
||||
return <TrackListItem
|
||||
index={index}
|
||||
track={track}
|
||||
/>
|
||||
})
|
||||
}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default ReleaseTracks
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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: <Icons.MdInfo />,
|
||||
render: BasicInformation,
|
||||
},
|
||||
{
|
||||
key: "tracks",
|
||||
label: "Tracks",
|
||||
icon: <Icons.MdLibraryMusic />,
|
||||
render: Tracks,
|
||||
},
|
||||
{
|
||||
key: "advanced",
|
||||
label: "Advanced",
|
||||
icon: <Icons.MdSettings />,
|
||||
render: Advanced,
|
||||
}
|
||||
]
|
@ -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 <div
|
||||
id={release._id}
|
||||
className="music-studio-page-release"
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
<div className="music-studio-page-release-title">
|
||||
<Image
|
||||
src={release.cover}
|
||||
/>
|
||||
|
||||
{release.title}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="music-studio-page-release-info"
|
||||
>
|
||||
<div className="music-studio-page-release-info-field">
|
||||
<Icons.IoMdMusicalNote />
|
||||
{release.type}
|
||||
</div>
|
||||
|
||||
<div className="music-studio-page-release-info-field">
|
||||
<Icons.MdTag />
|
||||
{release._id}
|
||||
</div>
|
||||
|
||||
{/* <div className="music-studio-page-release-info-field">
|
||||
<Icons.IoMdEye />
|
||||
{release.analytics?.listen_count ?? 0}
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default ReleaseItem
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
208
packages/app/src/components/MusicStudio/TrackEditor/index.jsx
Normal file
208
packages/app/src/components/MusicStudio/TrackEditor/index.jsx
Normal file
@ -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 <div className="track-editor">
|
||||
<div className="track-editor-field">
|
||||
<div className="track-editor-field-header">
|
||||
<Icons.MdImage />
|
||||
<span>Cover</span>
|
||||
</div>
|
||||
|
||||
<CoverEditor
|
||||
value={track.cover}
|
||||
onChange={(url) => handleChange("cover", url)}
|
||||
extraActions={[
|
||||
<antd.Button>
|
||||
Use Parent
|
||||
</antd.Button>
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="track-editor-field">
|
||||
<div className="track-editor-field-header">
|
||||
<Icons.MdOutlineMusicNote />
|
||||
<span>Title</span>
|
||||
</div>
|
||||
|
||||
<antd.Input
|
||||
value={track.title}
|
||||
placeholder="Track title"
|
||||
onChange={(e) => handleChange("title", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="track-editor-field">
|
||||
<div className="track-editor-field-header">
|
||||
<Icons.User />
|
||||
<span>Artist</span>
|
||||
</div>
|
||||
|
||||
<antd.Input
|
||||
value={track.artists.join(", ")}
|
||||
placeholder="Artist"
|
||||
onChange={(e) => handleChange("artist", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="track-editor-field">
|
||||
<div className="track-editor-field-header">
|
||||
<Icons.MdAlbum />
|
||||
<span>Album</span>
|
||||
</div>
|
||||
|
||||
<antd.Input
|
||||
value={track.album}
|
||||
placeholder="Album"
|
||||
onChange={(e) => handleChange("album", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="track-editor-field">
|
||||
<div className="track-editor-field-header">
|
||||
<Icons.MdExplicit />
|
||||
<span>Explicit</span>
|
||||
</div>
|
||||
|
||||
<antd.Switch
|
||||
checked={track.explicit}
|
||||
onChange={(value) => handleChange("explicit", value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<antd.Divider
|
||||
style={{
|
||||
margin: "5px 0",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="track-editor-field">
|
||||
<div className="track-editor-field-header">
|
||||
<Icons.TbMovie />
|
||||
<span>Edit Video</span>
|
||||
</div>
|
||||
|
||||
<antd.Button
|
||||
onClick={openVideoEditor}
|
||||
>
|
||||
Edit
|
||||
</antd.Button>
|
||||
</div>
|
||||
|
||||
<div className="track-editor-field">
|
||||
<div className="track-editor-field-header">
|
||||
<Icons.MdTextFormat />
|
||||
<span>Edit Lyrics</span>
|
||||
</div>
|
||||
|
||||
<antd.Button
|
||||
onClick={openLyricsEditor}
|
||||
>
|
||||
Edit
|
||||
</antd.Button>
|
||||
</div>
|
||||
|
||||
<div className="track-editor-field">
|
||||
<div className="track-editor-field-header">
|
||||
<Icons.MdTimeline />
|
||||
<span>Timestamps</span>
|
||||
</div>
|
||||
|
||||
<antd.Button
|
||||
disabled
|
||||
>
|
||||
Edit
|
||||
</antd.Button>
|
||||
</div>
|
||||
|
||||
<div className="track-editor-actions">
|
||||
<antd.Button
|
||||
type="text"
|
||||
icon={<Icons.MdClose />}
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</antd.Button>
|
||||
|
||||
<antd.Button
|
||||
type="primary"
|
||||
icon={<Icons.MdCheck />}
|
||||
onClick={onSave}
|
||||
>
|
||||
Save
|
||||
</antd.Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default TrackEditor
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 <div className="video-editor">
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
export default VideoEditor
|
@ -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 <antd.Result
|
||||
status="error"
|
||||
title="Error"
|
||||
subTitle={this.state.error}
|
||||
/>
|
||||
}
|
||||
|
||||
return <>
|
||||
{this.props.children}
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
export const Panel = (props) => {
|
||||
return <div
|
||||
{...props.props ?? {}}
|
||||
|
@ -2,6 +2,8 @@ import React from "react"
|
||||
import { Dropdown } from "antd"
|
||||
import { Icons } from "@components/Icons"
|
||||
|
||||
import checkUserIdIsSelf from "@utils/checkUserIdIsSelf"
|
||||
|
||||
import SaveButton from "./saveButton"
|
||||
import LikeButton from "./likeButton"
|
||||
import RepliesButton from "./replyButton"
|
||||
@ -110,7 +112,7 @@ export default (props) => {
|
||||
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)
|
||||
}
|
||||
|
@ -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 <div className="post-header" onDoubleClick={props.onDoubleClick}>
|
||||
{
|
||||
!props.disableReplyTag && props.postData.reply_to && <div
|
||||
@ -88,7 +62,9 @@ const PostCardHeader = (props) => {
|
||||
</h1>
|
||||
|
||||
<span className="post-header-user-info-timeago">
|
||||
{timeAgo}
|
||||
<TimeAgo
|
||||
time={props.postData.timestamp ?? props.postData.created_at}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
132
packages/app/src/components/ReleasesList/index.jsx
Normal file
132
packages/app/src/components/ReleasesList/index.jsx
Normal file
@ -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 <div className="playlistExplorer_section">
|
||||
<antd.Result
|
||||
status="warning"
|
||||
title="Failed to load"
|
||||
subTitle="We are sorry, but we could not load this requests. Please try again later."
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div className="music-releases-list">
|
||||
<div className="music-releases-list-header">
|
||||
<h1>
|
||||
{
|
||||
props.headerIcon
|
||||
}
|
||||
<Translation>
|
||||
{(t) => t(props.headerTitle)}
|
||||
</Translation>
|
||||
</h1>
|
||||
|
||||
<div className="music-releases-list-actions">
|
||||
<antd.Button
|
||||
icon={<Icons.MdChevronLeft />}
|
||||
onClick={onClickPrev}
|
||||
disabled={offset === 0 || loading}
|
||||
/>
|
||||
|
||||
<antd.Button
|
||||
icon={<Icons.MdChevronRight />}
|
||||
onClick={onClickNext}
|
||||
disabled={ended || loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="music-releases-list-items">
|
||||
{
|
||||
loading && <antd.Skeleton active />
|
||||
}
|
||||
{
|
||||
!loading && result.items.map((playlist, index) => {
|
||||
return <PlaylistItem
|
||||
key={index}
|
||||
playlist={playlist}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default ReleasesList
|
52
packages/app/src/components/ReleasesList/index.less
Normal file
52
packages/app/src/components/ReleasesList/index.less
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
33
packages/app/src/components/TimeAgo/index.jsx
Normal file
33
packages/app/src/components/TimeAgo/index.jsx
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
127
packages/app/src/cores/player/classes/TrackInstance.js
Normal file
127
packages/app/src/cores/player/classes/TrackInstance.js
Normal file
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
947
packages/app/src/cores/player/player.bkp.js
Executable file
947
packages/app/src/cores/player/player.bkp.js
Executable file
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -55,8 +55,6 @@ export default class SFXCore extends Core {
|
||||
src: [path],
|
||||
})
|
||||
}
|
||||
|
||||
this.console.log(this.soundsPool)
|
||||
}
|
||||
|
||||
async play(name, options = {}) {
|
||||
|
@ -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 <ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
...app.cores.style.getValue(),
|
||||
...app.cores.style.getVar(),
|
||||
},
|
||||
algorithm: themeAlgorithms,
|
||||
}}
|
||||
@ -69,8 +79,8 @@ export default class StyleCore extends Core {
|
||||
|
||||
static dependencies = ["settings"]
|
||||
|
||||
static themeManifestStorageKey = "theme"
|
||||
static modificationStorageKey = "themeModifications"
|
||||
static modificationStorageKey = "theme-modifications"
|
||||
static defaultVariantKey = "auto"
|
||||
|
||||
static get rootVariables() {
|
||||
let attributes = document.documentElement.getAttribute("style").trim().split(";")
|
||||
@ -87,142 +97,105 @@ export default class StyleCore extends Core {
|
||||
return Object.fromEntries(attributes)
|
||||
}
|
||||
|
||||
static get storagedTheme() {
|
||||
return store.get(StyleCore.themeManifestStorageKey)
|
||||
static get storagedVariantKey() {
|
||||
return app.cores.settings.get("style:theme_variant")
|
||||
}
|
||||
|
||||
static get storagedVariant() {
|
||||
return app.cores.settings.get("style:darkmode") ? "dark" : "light"
|
||||
static set storagedVariantKey(key) {
|
||||
return app.cores.settings.set("style:theme_variant", key)
|
||||
}
|
||||
|
||||
isOnTemporalVariant = false
|
||||
|
||||
// modifications
|
||||
static get storagedModifications() {
|
||||
return store.get(StyleCore.modificationStorageKey) ?? {}
|
||||
}
|
||||
|
||||
static set storagedModifications(modifications) {
|
||||
return store.set(StyleCore.modificationStorageKey, modifications)
|
||||
}
|
||||
|
||||
static get storagedModifications() {
|
||||
return store.get(StyleCore.modificationStorageKey) ?? {}
|
||||
}
|
||||
public = {
|
||||
theme: null,
|
||||
mutation: null,
|
||||
currentVariantKey: null,
|
||||
|
||||
static get variant() {
|
||||
if (window.app.cores.settings.is("style:auto_darkmode", true)) {
|
||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
return "dark"
|
||||
}
|
||||
}
|
||||
getVar: (...args) => 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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
|
39
packages/app/src/hooks/useClickNavById/index.js
Normal file
39
packages/app/src/hooks/useClickNavById/index.js
Normal file
@ -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
|
59
packages/app/src/hooks/useHideOnMouseStop/index.jsx
Normal file
59
packages/app/src/hooks/useHideOnMouseStop/index.jsx
Normal file
@ -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
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
</div>
|
||||
}
|
||||
|
||||
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 <>
|
||||
{
|
||||
|
@ -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,
|
||||
|
@ -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: <>
|
||||
<Icons.User />
|
||||
<Translation>
|
||||
{t => t("Account")}
|
||||
{t => t("Profile")}
|
||||
</Translation>
|
||||
</>,
|
||||
},
|
||||
{
|
||||
key: "studio",
|
||||
label: <>
|
||||
<Icons.MdHardware />
|
||||
<Translation>
|
||||
{t => t("Studio")}
|
||||
</Translation>
|
||||
</>,
|
||||
},
|
||||
{
|
||||
key: "addons",
|
||||
label: <>
|
||||
<Icons.Box />
|
||||
<Translation>
|
||||
{t => t("Addons")}
|
||||
</Translation>
|
||||
</>,
|
||||
},
|
||||
@ -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()}
|
||||
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -55,26 +55,28 @@ export default class ToolsBar extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Motion style={{
|
||||
x: spring(this.state.visible ? 0 : 100),
|
||||
width: spring(this.state.visible ? 100 : 0),
|
||||
}}>
|
||||
return <Motion
|
||||
style={{
|
||||
x: spring(this.state.visible ? 0 : 100),
|
||||
width: spring(this.state.visible ? 100 : 0),
|
||||
}}
|
||||
>
|
||||
{({ x, width }) => {
|
||||
return <div
|
||||
className="tools-bar-wrapper"
|
||||
style={{
|
||||
width: `${width}%`,
|
||||
transform: `translateX(${x}%)`,
|
||||
}}
|
||||
className={classnames(
|
||||
"tools-bar-wrapper",
|
||||
{
|
||||
visible: this.state.visible,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
id="tools_bar"
|
||||
className={classnames(
|
||||
"tools-bar",
|
||||
{
|
||||
visible: this.state.visible,
|
||||
}
|
||||
)}
|
||||
className="tools-bar"
|
||||
>
|
||||
{/* <div className="card" id="trendings">
|
||||
<div className="header">
|
||||
|
@ -6,15 +6,22 @@
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
max-width: 420px;
|
||||
min-width: 320px;
|
||||
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
|
||||
padding: 10px;
|
||||
max-width: 420px;
|
||||
|
||||
z-index: 150;
|
||||
padding: 10px;
|
||||
|
||||
.visible {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
&:not(.visible) {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.tools-bar {
|
||||
@ -29,13 +36,13 @@
|
||||
border-radius: @sidebar_borderRadius;
|
||||
box-shadow: @card-shadow;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
gap: 20px;
|
||||
|
||||
&.visible {
|
||||
padding: 10px;
|
||||
}
|
||||
flex: 0;
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
|
@ -85,7 +85,7 @@ export default (props) => {
|
||||
}
|
||||
}, [render])
|
||||
|
||||
const heightValue = visible ? Number(app.cores.style.defaultVar("top-bar-height").replace("px", "")) : 0
|
||||
const heightValue = visible ? Number(app.cores.style.getDefaultVar("top-bar-height").replace("px", "")) : 0
|
||||
|
||||
return <Motion style={{
|
||||
y: spring(visible ? 0 : 300,),
|
||||
|
@ -7,14 +7,14 @@ import Image from "@components/Image"
|
||||
import "./index.less"
|
||||
|
||||
const FieldItem = (props) => {
|
||||
return <div className="marketplace-field-item">
|
||||
<div className="marketplace-field-item-image">
|
||||
return <div className="addons-field-item">
|
||||
<div className="addons-field-item-image">
|
||||
<Image
|
||||
src={props.image}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="marketplace-field-item-info">
|
||||
<div className="addons-field-item-info">
|
||||
<h1>
|
||||
{props.title}
|
||||
</h1>
|
||||
@ -26,16 +26,16 @@ const FieldItem = (props) => {
|
||||
</div>
|
||||
}
|
||||
|
||||
const ExtensionsBrowser = () => {
|
||||
return <div className="marketplace-field">
|
||||
<div className="marketplace-field-header">
|
||||
const AddonsBrowser = () => {
|
||||
return <div className="addons-field">
|
||||
<div className="addons-field-header">
|
||||
<h1>
|
||||
<Icons.MdCode />
|
||||
Extensions
|
||||
Addons
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="marketplace-field-slider">
|
||||
<div className="addons-field-slider">
|
||||
<FieldItem
|
||||
title="Example Extension"
|
||||
description="Description"
|
||||
@ -60,22 +60,22 @@ const ExtensionsBrowser = () => {
|
||||
</div>
|
||||
}
|
||||
|
||||
const Marketplace = () => {
|
||||
return <div className="marketplace">
|
||||
<div className="marketplace-header">
|
||||
<div className="marketplace-header-card">
|
||||
const addons = () => {
|
||||
return <div className="addons-page">
|
||||
<div className="addons-header">
|
||||
<div className="addons-header-card">
|
||||
<h1>
|
||||
Marketplace
|
||||
Addons
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<SearchButton />
|
||||
</div>
|
||||
|
||||
<ExtensionsBrowser />
|
||||
<ExtensionsBrowser />
|
||||
<ExtensionsBrowser />
|
||||
<AddonsBrowser />
|
||||
<AddonsBrowser />
|
||||
<AddonsBrowser />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Marketplace
|
||||
export default addons
|
@ -1,4 +1,4 @@
|
||||
.marketplace {
|
||||
.addons-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
width: 100%;
|
||||
|
||||
.marketplace-header {
|
||||
.addons-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
width: 100%;
|
||||
|
||||
.marketplace-header-card {
|
||||
.addons-header-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@ -37,20 +37,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.marketplace-field {
|
||||
.addons-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.marketplace-field-header {}
|
||||
.addons-field-header {}
|
||||
|
||||
.marketplace-field-slider {
|
||||
.addons-field-slider {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.marketplace-field-item {
|
||||
.addons-field-item {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
@ -67,7 +67,7 @@
|
||||
|
||||
padding: 10px;
|
||||
|
||||
.marketplace-field-item-image {
|
||||
.addons-field-item-image {
|
||||
width: 100%;
|
||||
height: 60%;
|
||||
|
||||
@ -90,7 +90,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.marketplace-field-item-info {
|
||||
.addons-field-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -332,7 +332,7 @@ const FileListItem = (props) => {
|
||||
|
||||
export default (props) => {
|
||||
const onClickEditTrack = (track) => {
|
||||
app.DrawerController.open("track_editor", FileItemEditor, {
|
||||
app.layout.drawer.open("track_editor", FileItemEditor, {
|
||||
type: "drawer",
|
||||
props: {
|
||||
width: "30vw",
|
@ -1,8 +1,10 @@
|
||||
import React from "react"
|
||||
import { Tag } from "antd"
|
||||
import { Tag, Button } from "antd"
|
||||
import classnames from "classnames"
|
||||
import Marquee from "react-fast-marquee"
|
||||
|
||||
import useHideOnMouseStop from "@hooks/useHideOnMouseStop"
|
||||
|
||||
import { Icons } from "@components/Icons"
|
||||
import Controls from "@components/Player/Controls"
|
||||
|
||||
@ -49,6 +51,7 @@ const PlayerController = React.forwardRef((props, ref) => {
|
||||
|
||||
const titleRef = React.useRef()
|
||||
|
||||
const [hide, onMouseEnter, onMouseLeave] = useHideOnMouseStop({ delay: 3000, hideCursor: true })
|
||||
const [titleIsOverflown, setTitleIsOverflown] = React.useState(false)
|
||||
|
||||
const [currentTime, setCurrentTime] = React.useState(0)
|
||||
@ -61,6 +64,7 @@ const PlayerController = React.forwardRef((props, ref) => {
|
||||
setDraggingTime(false)
|
||||
|
||||
app.cores.player.seek(seekTime)
|
||||
syncPlayback()
|
||||
}
|
||||
|
||||
async function syncPlayback() {
|
||||
@ -87,12 +91,24 @@ const PlayerController = React.forwardRef((props, ref) => {
|
||||
React.useEffect(() => {
|
||||
setTitleIsOverflown(isOverflown(titleRef.current))
|
||||
setTrackDuration(app.cores.player.duration())
|
||||
console.log(context.track_manifest)
|
||||
}, [context.track_manifest])
|
||||
|
||||
React.useEffect(() => {
|
||||
syncPlayback()
|
||||
}, [])
|
||||
|
||||
const isStopped = context.playback_status === "stopped"
|
||||
|
||||
return <div
|
||||
className="lyrics-player-controller-wrapper"
|
||||
className={classnames(
|
||||
"lyrics-player-controller-wrapper",
|
||||
{
|
||||
["hidden"]: hide,
|
||||
}
|
||||
)}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<div className="lyrics-player-controller">
|
||||
<div className="lyrics-player-controller-info">
|
||||
@ -174,7 +190,6 @@ const PlayerController = React.forwardRef((props, ref) => {
|
||||
<div className="lyrics-player-controller-tags">
|
||||
{
|
||||
context.track_manifest?.metadata.lossless && <Tag
|
||||
color="geekblue"
|
||||
icon={<Icons.TbWaveSine />}
|
||||
bordered={false}
|
||||
>
|
||||
@ -188,6 +203,22 @@ const PlayerController = React.forwardRef((props, ref) => {
|
||||
Explicit
|
||||
</Tag>
|
||||
}
|
||||
{
|
||||
props.lyrics?.sync_audio_at && <Tag
|
||||
bordered={false}
|
||||
icon={<Icons.TbMovie />}
|
||||
>
|
||||
Video
|
||||
</Tag>
|
||||
}
|
||||
{
|
||||
props.lyrics?.available_langs && <Button
|
||||
icon={<Icons.MdTranslate />}
|
||||
type={props.translationEnabled ? "primary" : "default"}
|
||||
onClick={() => props.toggleTranslationEnabled()}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,6 +54,8 @@ const LyricsText = React.forwardRef((props, textRef) => {
|
||||
if (currentLineIndex === 0) {
|
||||
setVisible(false)
|
||||
} else {
|
||||
setVisible(true)
|
||||
console.log(`Scrolling to line ${currentLineIndex}`)
|
||||
// find line element by id
|
||||
const lineElement = textRef.current.querySelector(`#lyrics-line-${currentLineIndex}`)
|
||||
|
||||
@ -63,24 +65,26 @@ const LyricsText = React.forwardRef((props, textRef) => {
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
})
|
||||
} else {
|
||||
// scroll to top
|
||||
textRef.current.scrollTop = 0
|
||||
}
|
||||
}
|
||||
}, [currentLineIndex])
|
||||
|
||||
//* Handle when playback status change
|
||||
React.useEffect(() => {
|
||||
if (lyrics) {
|
||||
if (typeof lyrics?.lrc !== "undefined") {
|
||||
if (context.playback_status === "playing") {
|
||||
startSyncInterval()
|
||||
} else {
|
||||
if (syncInterval) {
|
||||
clearInterval(syncInterval)
|
||||
}
|
||||
} startSyncInterval()
|
||||
if (typeof lyrics?.lrc !== "undefined") {
|
||||
if (context.playback_status === "playing") {
|
||||
startSyncInterval()
|
||||
} else {
|
||||
if (syncInterval) {
|
||||
clearInterval(syncInterval)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clearInterval(syncInterval)
|
||||
}
|
||||
|
||||
}, [context.playback_status])
|
||||
|
||||
//* Handle when lyrics object change
|
||||
@ -96,6 +100,12 @@ const LyricsText = React.forwardRef((props, textRef) => {
|
||||
}
|
||||
}, [lyrics])
|
||||
|
||||
React.useEffect(() => {
|
||||
setVisible(false)
|
||||
clearInterval(syncInterval)
|
||||
setCurrentLineIndex(0)
|
||||
}, [context.track_manifest])
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
clearInterval(syncInterval)
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from "react"
|
||||
|
||||
import classnames from "classnames"
|
||||
import useHideOnMouseStop from "@hooks/useHideOnMouseStop"
|
||||
import { Context } from "@contexts/WithPlayerContext"
|
||||
|
||||
const maxLatencyInMs = 55
|
||||
@ -32,6 +34,13 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
||||
|
||||
// if `sync_audio_at_ms` is present, it means the video must be synced with audio
|
||||
if (lyrics.video_source && typeof lyrics.sync_audio_at_ms !== "undefined") {
|
||||
if (!videoRef.current) {
|
||||
clearInterval(syncInterval)
|
||||
setSyncInterval(null)
|
||||
setCurrentVideoLatency(0)
|
||||
return false
|
||||
}
|
||||
|
||||
const currentTrackTime = app.cores.player.seek()
|
||||
const currentVideoTime = videoRef.current.currentTime - (lyrics.sync_audio_at_ms / 1000)
|
||||
|
||||
@ -55,7 +64,7 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
||||
}
|
||||
|
||||
function startSyncInterval() {
|
||||
setSyncInterval(setInterval(syncPlayback, 100))
|
||||
setSyncInterval(setInterval(syncPlayback, 300))
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -97,6 +106,17 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
||||
}
|
||||
}, [context.playback_status])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (context.loading === true && context.playback_status === "playing") {
|
||||
videoRef.current.pause()
|
||||
}
|
||||
|
||||
if (context.loading === false && context.playback_status === "playing") {
|
||||
videoRef.current.play()
|
||||
}
|
||||
|
||||
}, [context.loading])
|
||||
|
||||
//* Handle when lyrics object change
|
||||
React.useEffect(() => {
|
||||
if (lyrics) {
|
||||
@ -141,17 +161,24 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
|
||||
}, [])
|
||||
|
||||
return <>
|
||||
<div className="videoDebugOverlay">
|
||||
<div>
|
||||
<p>Maximun latency</p>
|
||||
<p>{maxLatencyInMs}ms</p>
|
||||
{
|
||||
props.lyrics?.sync_audio_at && <div
|
||||
className={classnames(
|
||||
"videoDebugOverlay",
|
||||
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<p>Maximun latency</p>
|
||||
<p>{maxLatencyInMs}ms</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Video Latency</p>
|
||||
<p>{(currentVideoLatency * 1000).toFixed(2)}ms</p>
|
||||
</div>
|
||||
{syncingVideo ? <p>Syncing video...</p> : null}
|
||||
</div>
|
||||
<div>
|
||||
<p>Video Latency</p>
|
||||
<p>{(currentVideoLatency * 1000).toFixed(2)}ms</p>
|
||||
</div>
|
||||
{syncingVideo ? <p>Syncing video...</p> : null}
|
||||
</div>
|
||||
}
|
||||
|
||||
<video
|
||||
className="lyrics-video"
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react"
|
||||
import classnames from "classnames"
|
||||
|
||||
import useMaxScreen from "@utils/useMaxScreen"
|
||||
import useMaxScreen from "@hooks/useMaxScreen"
|
||||
import { WithPlayerContext, Context } from "@contexts/WithPlayerContext"
|
||||
|
||||
import MusicService from "@models/music"
|
||||
@ -14,23 +14,42 @@ import "./index.less"
|
||||
|
||||
const EnchancedLyrics = (props) => {
|
||||
const context = React.useContext(Context)
|
||||
|
||||
const [initialized, setInitialized] = React.useState(false)
|
||||
const [lyrics, setLyrics] = React.useState(null)
|
||||
const [translationEnabled, setTranslationEnabled] = React.useState(false)
|
||||
|
||||
const videoRef = React.useRef()
|
||||
const textRef = React.useRef()
|
||||
|
||||
async function loadLyrics(track_id) {
|
||||
const result = await MusicService.getTrackLyrics(track_id)
|
||||
const result = await MusicService.getTrackLyrics(track_id, {
|
||||
preferTranslation: translationEnabled,
|
||||
})
|
||||
|
||||
if (result) {
|
||||
setLyrics(result)
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTranslationEnabled(to) {
|
||||
setTranslationEnabled((prev) => {
|
||||
return to ?? !prev
|
||||
})
|
||||
}
|
||||
|
||||
useMaxScreen()
|
||||
|
||||
React.useEffect((prev) => {
|
||||
if (initialized) {
|
||||
loadLyrics(context.track_manifest._id)
|
||||
}
|
||||
}, [translationEnabled])
|
||||
|
||||
//* Handle when context change track_manifest
|
||||
React.useEffect(() => {
|
||||
setLyrics(null)
|
||||
|
||||
if (context.track_manifest) {
|
||||
loadLyrics(context.track_manifest._id)
|
||||
}
|
||||
@ -41,6 +60,10 @@ const EnchancedLyrics = (props) => {
|
||||
console.log(lyrics)
|
||||
}, [lyrics])
|
||||
|
||||
React.useEffect(() => {
|
||||
setInitialized(true)
|
||||
}, [])
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"lyrics",
|
||||
@ -57,10 +80,13 @@ const EnchancedLyrics = (props) => {
|
||||
<LyricsText
|
||||
ref={textRef}
|
||||
lyrics={lyrics}
|
||||
translationEnabled={translationEnabled}
|
||||
/>
|
||||
|
||||
<PlayerController
|
||||
|
||||
lyrics={lyrics}
|
||||
translationEnabled={translationEnabled}
|
||||
toggleTranslationEnabled={toggleTranslationEnabled}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
@ -77,6 +77,12 @@
|
||||
|
||||
padding: 60px;
|
||||
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.lyrics-player-controller {
|
||||
position: relative;
|
||||
|
||||
@ -256,5 +262,11 @@
|
||||
|
||||
width: 200px;
|
||||
height: fit-content;
|
||||
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,12 +2,13 @@ import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
|
||||
import { Icons } from "@components/Icons"
|
||||
import UserPreview from "@components/UserPreview"
|
||||
|
||||
import useChat from "@hooks/useChat"
|
||||
import ChatsService from "@models/chats"
|
||||
|
||||
import lodash from "lodash"
|
||||
import ChatsService from "@models/chats"
|
||||
import UserService from "@models/user"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
@ -19,7 +20,16 @@ const ChatPage = (props) => {
|
||||
const [isOnBottomView, setIsOnBottomView] = React.useState(true)
|
||||
const [currentText, setCurrentText] = React.useState("")
|
||||
|
||||
const [L_History, R_History, E_History, M_History] = app.cores.api.useRequest(ChatsService.getChatHistory, to_user_id)
|
||||
const [L_User, R_User, E_User, M_User] = app.cores.api.useRequest(
|
||||
UserService.data,
|
||||
{
|
||||
user_id: to_user_id
|
||||
}
|
||||
)
|
||||
const [L_History, R_History, E_History, M_History] = app.cores.api.useRequest(
|
||||
ChatsService.getChatHistory,
|
||||
to_user_id
|
||||
)
|
||||
|
||||
const {
|
||||
sendMessage,
|
||||
@ -30,7 +40,12 @@ const ChatPage = (props) => {
|
||||
isRemoteTyping,
|
||||
} = useChat(to_user_id)
|
||||
|
||||
async function submitMessage() {
|
||||
|
||||
console.log(R_User)
|
||||
|
||||
async function submitMessage(e) {
|
||||
e.preventDefault()
|
||||
|
||||
if (!currentText) {
|
||||
return false
|
||||
}
|
||||
@ -88,12 +103,8 @@ const ChatPage = (props) => {
|
||||
>
|
||||
<div className="chat-page-header">
|
||||
<UserPreview
|
||||
user_id={to_user_id}
|
||||
user={R_User}
|
||||
/>
|
||||
|
||||
{
|
||||
isRemoteTyping && <p>Typing...</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -147,13 +158,27 @@ const ChatPage = (props) => {
|
||||
|
||||
<div className="chat-page-input-wrapper">
|
||||
<div className="chat-page-input">
|
||||
<antd.Input
|
||||
<antd.Input.TextArea
|
||||
placeholder="Enter message"
|
||||
value={currentText}
|
||||
onChange={onInputChange}
|
||||
onPressEnter={submitMessage}
|
||||
autoSize
|
||||
maxLength={1024}
|
||||
maxRows={3}
|
||||
/>
|
||||
<antd.Button
|
||||
type="primary"
|
||||
icon={<Icons.Send />}
|
||||
onClick={submitMessage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
isRemoteTyping && R_User && <div className="chat-page-remote-typing">
|
||||
<span>{R_User.username} is typing...</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@ -93,16 +93,64 @@
|
||||
}
|
||||
|
||||
.chat-page-input-wrapper {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
|
||||
.chat-page-remote-typing {
|
||||
position: absolute;
|
||||
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
transform: translateY(120%);
|
||||
|
||||
padding: 4px 10px;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.chat-page-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border: 2px var(--border-color) solid;
|
||||
border-radius: 12px;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
.ant-input {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
|
||||
background-color: transparent;
|
||||
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,82 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
|
||||
|
||||
import ChatsService from "@models/chats"
|
||||
|
||||
import TimeAgo from "@components/TimeAgo"
|
||||
import Image from "@components/Image"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const ChatPreview = (props) => {
|
||||
const { chat } = props
|
||||
|
||||
const previewUserId = chat.from_user_id === app.userData._id ? chat.to_user_id : chat.from_user_id
|
||||
|
||||
return <div
|
||||
className="chat-preview"
|
||||
onClick={() => {
|
||||
app.location.push(`/messages/${previewUserId}`)
|
||||
}}
|
||||
>
|
||||
<div className="chat-preview-image">
|
||||
<Image
|
||||
src={chat.user.avatar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="chat-preview-content">
|
||||
<div className="chat-preview-username">
|
||||
@{chat.user.username}
|
||||
</div>
|
||||
<div className="chat-preview-text" >
|
||||
<p>
|
||||
{chat.content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="chat-preview-date">
|
||||
<span>
|
||||
<TimeAgo
|
||||
time={chat.created_at}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const MessagesPage = (props) => {
|
||||
const [L_Recent, R_Recent, E_Recent, M_Recent] = app.cores.api.useRequest(ChatsService.getRecentChats)
|
||||
|
||||
console.log(R_Recent, E_Recent)
|
||||
|
||||
if (E_Recent) {
|
||||
return <antd.Result
|
||||
status="warning"
|
||||
title="Error"
|
||||
subTitle={E_Recent.message}
|
||||
/>
|
||||
}
|
||||
|
||||
if (L_Recent) {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
return <div
|
||||
className="messages-page"
|
||||
>
|
||||
<h1>Recent Messages</h1>
|
||||
|
||||
{
|
||||
R_Recent.map((chat) => {
|
||||
return <ChatPreview
|
||||
key={chat._id}
|
||||
chat={chat}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,78 @@
|
||||
.messages-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-preview {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chat-preview-image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-preview-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
gap: 5px;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.chat-preview-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
p {
|
||||
overflow: hidden;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-preview-date {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import React from "react"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
export default (props) => {
|
||||
return <div className="music-dashboard">
|
||||
<div className="music-dashboard_header">
|
||||
<h1>Your Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div className="music-dashboard_content">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -1,206 +0,0 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
|
||||
import { Icons } from "@components/Icons"
|
||||
import ImageViewer from "@components/ImageViewer"
|
||||
import Searcher from "@components/Searcher"
|
||||
|
||||
import ReleaseCreator from "../../creator"
|
||||
|
||||
import MusicModel from "@models/music"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const ReleaseItem = (props) => {
|
||||
const { key, release } = props
|
||||
|
||||
return <div
|
||||
className="music_panel_releases_item"
|
||||
key={key}
|
||||
id={key}
|
||||
>
|
||||
<div
|
||||
className="music_panel_releases_info"
|
||||
>
|
||||
<div
|
||||
className="music_panel_releases_info_cover"
|
||||
>
|
||||
<ImageViewer
|
||||
src={release.cover ?? release.thumbnail ?? "/assets/no_song.png"}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="music_panel_releases_info_data"
|
||||
>
|
||||
<h1>
|
||||
{release.title}
|
||||
</h1>
|
||||
|
||||
{
|
||||
release.description && <h4>
|
||||
{release.description}
|
||||
</h4>
|
||||
}
|
||||
|
||||
<div className="music_panel_releases_info_extra">
|
||||
{
|
||||
release.public
|
||||
? <>
|
||||
<Icons.MdOutlinePublic />
|
||||
<span>
|
||||
Public
|
||||
</span>
|
||||
</>
|
||||
: <>
|
||||
<Icons.MdOutlineLock />
|
||||
<span>
|
||||
Private
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="music_panel_releases_actions"
|
||||
>
|
||||
<antd.Button
|
||||
onClick={props.onClickNavigate}
|
||||
>
|
||||
Open
|
||||
</antd.Button>
|
||||
|
||||
<antd.Button
|
||||
onClick={props.onClickEditTrack}
|
||||
icon={<Icons.Edit />}
|
||||
>
|
||||
Modify
|
||||
</antd.Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const openReleaseCreator = ({
|
||||
release_id = null,
|
||||
onModification = () => { }
|
||||
} = {}) => {
|
||||
console.log("Opening release creator", release_id)
|
||||
|
||||
app.DrawerController.open("release_creator", ReleaseCreator, {
|
||||
type: "drawer",
|
||||
props: {
|
||||
title: <h2
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
<Icons.MdOutlineQueueMusic />
|
||||
Creator
|
||||
</h2>,
|
||||
width: "fit-content",
|
||||
},
|
||||
componentProps: {
|
||||
release_id: release_id,
|
||||
onModification: onModification,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const navigateToRelease = (release_id) => {
|
||||
return app.location.push(`/play/${release_id}`)
|
||||
}
|
||||
|
||||
export default (props) => {
|
||||
const [searchResults, setSearchResults] = React.useState(null)
|
||||
const [L_Releases, R_Releases, E_Releases, M_Releases] = app.cores.api.useRequest(MusicModel.getMyReleases)
|
||||
|
||||
if (E_Releases) {
|
||||
console.error(E_Releases)
|
||||
|
||||
return <antd.Result
|
||||
status="warning"
|
||||
title="Failed to load"
|
||||
subTitle="We are sorry, but we could not load your releases. Please try again later."
|
||||
/>
|
||||
}
|
||||
|
||||
if (L_Releases) {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
return <div
|
||||
className="music_panel_creator"
|
||||
>
|
||||
<div className="music_panel_releases_header">
|
||||
<h1>
|
||||
<Icons.Music />
|
||||
Your releases
|
||||
</h1>
|
||||
|
||||
<div className="music_panel_releases_header_actions">
|
||||
<antd.Button
|
||||
onClick={() => openReleaseCreator({
|
||||
onModification: M_Releases,
|
||||
})}
|
||||
icon={<Icons.Plus />}
|
||||
type="primary"
|
||||
disabled={app.isMobile}
|
||||
>
|
||||
New release
|
||||
</antd.Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Searcher
|
||||
small
|
||||
renderResults={false}
|
||||
model={MusicModel.getMyReleases}
|
||||
onSearchResult={setSearchResults}
|
||||
onEmpty={() => setSearchResults(null)}
|
||||
/>
|
||||
|
||||
<div className="music_panel_releases_list">
|
||||
{
|
||||
searchResults?.items && searchResults.items.length === 0 && <antd.Result
|
||||
status="info"
|
||||
title="No results"
|
||||
subTitle="We are sorry, but we could not find any results for your search."
|
||||
/>
|
||||
}
|
||||
{
|
||||
searchResults?.items && searchResults.items.length > 0 && searchResults.items.map((release) => {
|
||||
return <ReleaseItem
|
||||
key={release._id}
|
||||
release={release}
|
||||
onClickEditTrack={() => openReleaseCreator({
|
||||
release_id: release._id,
|
||||
onModification: M_Releases,
|
||||
})}
|
||||
onClickNavigate={() => navigateToRelease(release._id)}
|
||||
/>
|
||||
})
|
||||
}
|
||||
{
|
||||
!searchResults && R_Releases.items.length === 0 && <antd.Result
|
||||
status="info"
|
||||
title="No releases"
|
||||
subTitle="You don't have any releases yet."
|
||||
/>
|
||||
}
|
||||
{
|
||||
!searchResults && R_Releases.items.map((release) => {
|
||||
return <ReleaseItem
|
||||
key={release._id}
|
||||
release={release}
|
||||
onClickEditTrack={() => openReleaseCreator({
|
||||
release_id: release._id,
|
||||
onModification: M_Releases,
|
||||
})}
|
||||
onClickNavigate={() => navigateToRelease(release._id)}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
.music_panel_creator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: overlay;
|
||||
|
||||
.music_panel_releases_header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.music_panel_releases_header_actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.ant-btn {
|
||||
margin-left: 10px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.music_panel_releases_list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
padding-bottom: 20px;
|
||||
|
||||
.music_panel_releases_item {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.music_panel_releases_info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
height: 100%;
|
||||
max-width: 65%;
|
||||
|
||||
.music_panel_releases_info_cover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin-right: 10px;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.music_panel_releases_info_data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 80%;
|
||||
|
||||
h1 {
|
||||
margin: 0 !important;
|
||||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 !important;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.music_panel_releases_info_extra {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.music_panel_releases_actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
margin-left: 10px;
|
||||
|
||||
.ant-btn {
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ import { Icons, createIconRender } from "@components/Icons"
|
||||
import MusicTrack from "@components/Music/Track"
|
||||
import PlaylistItem from "@components/Music/PlaylistItem"
|
||||
|
||||
import ReleasesList from "@components/ReleasesList"
|
||||
|
||||
import FeedModel from "@models/feed"
|
||||
import MusicModel from "@models/music"
|
||||
|
||||
@ -62,133 +64,12 @@ const MusicNavbar = (props) => {
|
||||
useUrlQuery
|
||||
renderResults={false}
|
||||
model={MusicModel.search}
|
||||
modelParams={{
|
||||
useTidal: app.cores.sync.getActiveLinkedServices().tidal,
|
||||
}}
|
||||
onSearchResult={props.setSearchResults}
|
||||
onEmpty={() => props.setSearchResults(false)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
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) {
|
||||
setEnded(result.length < hopNumber)
|
||||
}
|
||||
}, [result])
|
||||
|
||||
if (error) {
|
||||
console.error(error)
|
||||
|
||||
return <div className="playlistExplorer_section">
|
||||
<antd.Result
|
||||
status="warning"
|
||||
title="Failed to load"
|
||||
subTitle="We are sorry, but we could not load this requests. Please try again later."
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div className="playlistExplorer_section">
|
||||
<div className="playlistExplorer_section_header">
|
||||
<h1>
|
||||
{
|
||||
props.headerIcon
|
||||
}
|
||||
<Translation>
|
||||
{(t) => t(props.headerTitle)}
|
||||
</Translation>
|
||||
</h1>
|
||||
|
||||
<div className="playlistExplorer_section_header_actions">
|
||||
<antd.Button
|
||||
icon={<Icons.MdChevronLeft />}
|
||||
onClick={onClickPrev}
|
||||
disabled={offset === 0 || loading}
|
||||
/>
|
||||
|
||||
<antd.Button
|
||||
icon={<Icons.MdChevronRight />}
|
||||
onClick={onClickNext}
|
||||
disabled={ended || loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="playlistExplorer_section_list">
|
||||
{
|
||||
loading && <antd.Skeleton active />
|
||||
}
|
||||
{
|
||||
!loading && result.items.map((playlist, index) => {
|
||||
return <PlaylistItem
|
||||
key={index}
|
||||
playlist={playlist}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const ResultGroupsDecorators = {
|
||||
"playlists": {
|
||||
icon: "MdPlaylistPlay",
|
@ -150,59 +150,6 @@ html {
|
||||
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
.playlistExplorer_section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
overflow-x: visible;
|
||||
|
||||
.playlistExplorer_section_header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
margin-bottom: 20px;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.playlistExplorer_section_header_actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
align-self: center;
|
||||
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.playlistExplorer_section_list {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.music-explorer_search_results {
|
@ -1,6 +1,6 @@
|
||||
import LibraryTab from "./components/library"
|
||||
import FavoritesTab from "./components/favorites"
|
||||
import ExploreTab from "./components/explore"
|
||||
import LibraryTab from "./library"
|
||||
import FavoritesTab from "./favorites"
|
||||
import ExploreTab from "./explore"
|
||||
|
||||
export default [
|
||||
{
|
@ -1,7 +1,6 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
|
||||
import { Translation } from "react-i18next"
|
||||
import { SliderPicker } from "react-color"
|
||||
|
||||
@ -9,6 +8,19 @@ import { Icons, createIconRender } from "@components/Icons"
|
||||
|
||||
import PerformanceLog from "@classes/PerformanceLog"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
function shouldUseHorizontalLayout(type) {
|
||||
switch (type) {
|
||||
case "switch":
|
||||
return true
|
||||
case "button":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const SettingsComponents = {
|
||||
button: {
|
||||
component: antd.Button,
|
||||
@ -400,8 +412,10 @@ export default class SettingItemComponent extends React.PureComponent {
|
||||
className={classnames(
|
||||
"setting_item",
|
||||
{
|
||||
["usePadding"]: this.props.setting.usePadding ?? true
|
||||
})}
|
||||
["usePadding"]: this.props.setting.usePadding ?? true,
|
||||
["useHorizontal"]: this.props.setting.layout ?? shouldUseHorizontalLayout(String(this.props.setting.component).toLowerCase())
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="setting_item_header">
|
||||
<div className="setting_item_info">
|
||||
@ -414,7 +428,9 @@ export default class SettingItemComponent extends React.PureComponent {
|
||||
{(t) => t(this.props.setting.title ?? this.props.setting.id)}
|
||||
</Translation>
|
||||
</h1>
|
||||
{this.props.setting.experimental && <antd.Tag> Experimental </antd.Tag>}
|
||||
{
|
||||
this.props.setting.experimental && <antd.Tag> Experimental </antd.Tag>
|
||||
}
|
||||
</div>
|
||||
<div className="setting_item_header_description">
|
||||
<p>
|
||||
|
@ -0,0 +1,121 @@
|
||||
.setting_item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
padding: 0 15px;
|
||||
|
||||
&.useHorizontal {
|
||||
flex-direction: row;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 50px;
|
||||
|
||||
.setting_item_content {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.uploadButton {
|
||||
background-color: var(--background-color-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.setting_item_header {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
|
||||
.setting_item_info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 7px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
.setting_item_header_title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
|
||||
color: var(--background-color-contrast);
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
color: var(--background-color-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
.setting_item_header_description {
|
||||
p {
|
||||
color: var(--background-color-contrast);
|
||||
font-size: 0.7rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.setting_item_header_actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
.ant-btn {
|
||||
background-color: var(--background-color-primary-2);
|
||||
}
|
||||
|
||||
.ant-btn:not([disabled]) {
|
||||
&:hover {
|
||||
color: var(--colorPrimary);
|
||||
border: 1px solid var(--colorPrimary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting_item_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
--ignore-dragger: true;
|
||||
padding: 6px 20px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
h3,
|
||||
p,
|
||||
span {
|
||||
color: var(--background-color-contrast);
|
||||
}
|
||||
|
||||
button {
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-icon-only {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.ant-select {}
|
||||
}
|
||||
}
|
@ -122,6 +122,14 @@ export default () => {
|
||||
})
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
app.layout.tools_bar.toggleVisibility(false)
|
||||
|
||||
return () => {
|
||||
app.layout.tools_bar.toggleVisibility(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div className="settings_wrapper">
|
||||
<div className="settings_menu">
|
||||
<antd.Menu
|
||||
|
@ -46,7 +46,7 @@
|
||||
|
||||
width: 700px;
|
||||
|
||||
gap: 20px;
|
||||
gap: 10px;
|
||||
|
||||
.settings_content_group {
|
||||
position: relative;
|
||||
@ -61,7 +61,7 @@
|
||||
|
||||
padding: 20px;
|
||||
|
||||
gap: 20px;
|
||||
gap: 15px;
|
||||
|
||||
.settings_content_group_header {
|
||||
position: relative;
|
||||
@ -86,106 +86,6 @@
|
||||
gap: 30px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
.setting_item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
padding: 0 20px;
|
||||
|
||||
.uploadButton{
|
||||
background-color: var(--background-color-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.setting_item_header {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
|
||||
.setting_item_info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
.setting_item_header_title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
|
||||
color: var(--background-color-contrast);
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
color: var(--background-color-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
.setting_item_header_description {
|
||||
p {
|
||||
color: var(--background-color-contrast);
|
||||
font-size: 0.7rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.setting_item_header_actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
.ant-btn {
|
||||
background-color: var(--background-color-primary-2);
|
||||
}
|
||||
|
||||
.ant-btn:not([disabled]) {
|
||||
&:hover {
|
||||
color: var(--colorPrimary);
|
||||
border: 1px solid var(--colorPrimary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting_item_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
--ignore-dragger: true;
|
||||
padding: 6px 20px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
h1,h2,h3,h4,h5,h6,h3,p,span {
|
||||
color: var(--background-color-contrast);
|
||||
}
|
||||
|
||||
button {
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.ant-btn.ant-btn-icon-only {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.ant-select {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
55
packages/app/src/pages/studio/index.jsx
Normal file
55
packages/app/src/pages/studio/index.jsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from "react"
|
||||
|
||||
import { Icons } from "@components/Icons"
|
||||
|
||||
import useClickNavById from "@hooks/useClickNavById"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const SelectorPaths = {
|
||||
"music": "/studio/music",
|
||||
"tv": "/studio/tv",
|
||||
"marketplace": "/studio/marketplace",
|
||||
}
|
||||
|
||||
const StudioPage = () => {
|
||||
const [navigatorRef, navigatorProps] = useClickNavById(SelectorPaths, ".studio-page-selectors-item")
|
||||
|
||||
return <div className="studio-page">
|
||||
<div className="studio-page-header">
|
||||
<h1>Studio</h1>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="studio-page-selectors"
|
||||
ref={navigatorRef}
|
||||
{...navigatorProps}
|
||||
>
|
||||
<div
|
||||
id="music"
|
||||
className="studio-page-selectors-item"
|
||||
>
|
||||
<Icons.MdLibraryMusic />
|
||||
<span>Music</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="tv"
|
||||
className="studio-page-selectors-item"
|
||||
>
|
||||
<Icons.MdTv />
|
||||
<span>TV</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="marketplace"
|
||||
className="studio-page-selectors-item"
|
||||
>
|
||||
<Icons.MdCode />
|
||||
<span>Marketplace</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default StudioPage
|
78
packages/app/src/pages/studio/index.less
Normal file
78
packages/app/src/pages/studio/index.less
Normal file
@ -0,0 +1,78 @@
|
||||
@studio-page-selectors-item-size: 100px;
|
||||
|
||||
.studio-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
.studio-page-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 10px 20px;
|
||||
gap: 10px;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
border-radius: 12px;
|
||||
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-page-selectors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
.studio-page-selectors-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: @studio-page-selectors-item-size;
|
||||
max-height: @studio-page-selectors-item-size;
|
||||
min-height: @studio-page-selectors-item-size;
|
||||
min-width: @studio-page-selectors-item-size;
|
||||
|
||||
padding: 20px;
|
||||
gap: 10px;
|
||||
|
||||
border: 2px var(--border-color) solid;
|
||||
border-radius: 12px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-color-accent);
|
||||
}
|
||||
|
||||
svg,
|
||||
span {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
packages/app/src/pages/studio/music/[release_id]/index.jsx
Normal file
13
packages/app/src/pages/studio/music/[release_id]/index.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from "react"
|
||||
|
||||
import ReleaseEditor from "@components/MusicStudio/ReleaseEditor"
|
||||
|
||||
const ReleaseEditorPage = (props) => {
|
||||
const { release_id } = props.params
|
||||
|
||||
return <ReleaseEditor
|
||||
release_id={release_id}
|
||||
/>
|
||||
}
|
||||
|
||||
export default ReleaseEditorPage
|
32
packages/app/src/pages/studio/music/index.jsx
Normal file
32
packages/app/src/pages/studio/music/index.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
|
||||
import { Icons } from "@components/Icons"
|
||||
|
||||
import MyReleasesList from "@components/MusicStudio/MyReleasesList"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const MusicStudioPage = (props) => {
|
||||
return <div
|
||||
className="music-studio-page"
|
||||
>
|
||||
<div className="music-studio-page-header">
|
||||
<h1>Music Studio</h1>
|
||||
|
||||
<antd.Button
|
||||
type="primary"
|
||||
icon={<Icons.PlusCircle />}
|
||||
onClick={() => {
|
||||
app.location.push("/studio/music/new")
|
||||
}}
|
||||
>
|
||||
New Release
|
||||
</antd.Button>
|
||||
</div>
|
||||
|
||||
<MyReleasesList />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default MusicStudioPage
|
25
packages/app/src/pages/studio/music/index.less
Normal file
25
packages/app/src/pages/studio/music/index.less
Normal file
@ -0,0 +1,25 @@
|
||||
.music-studio-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
.music-studio-page-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.music-studio-page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@ -256,15 +256,16 @@ export default class StreamViewer extends React.Component {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
exitPlayerAnimation = () => {
|
||||
app.cores.style.applyInitialVariant()
|
||||
app.cores.style.compactMode(false)
|
||||
app.cores.style.applyVariant(app.cores.style.currentVariantKey)
|
||||
app.cores.style.toggleCompactMode(false)
|
||||
app.layout.toggleCenteredContent(true)
|
||||
app.controls.toggleUIVisibility(true)
|
||||
}
|
||||
|
||||
|
40
packages/app/src/settings/accessibility/index.jsx
Normal file
40
packages/app/src/settings/accessibility/index.jsx
Normal file
@ -0,0 +1,40 @@
|
||||
export default {
|
||||
id: "accessibility",
|
||||
icon: "MdAccessibilityNew",
|
||||
label: "Accessibility",
|
||||
group: "app",
|
||||
order: 4,
|
||||
settings: [
|
||||
{
|
||||
id: "haptics:enabled",
|
||||
storaged: true,
|
||||
group: "Accessibility",
|
||||
component: "Switch",
|
||||
icon: "MdVibration",
|
||||
title: "Haptic Feedback",
|
||||
description: "Enable haptic feedback on touch events.",
|
||||
desktop: false
|
||||
},
|
||||
{
|
||||
id: "longPressDelay",
|
||||
storaged: true,
|
||||
group: "Accessibility",
|
||||
component: "Slider",
|
||||
icon: "MdTimer",
|
||||
title: "Long press delay",
|
||||
description: "Set the delay before long press trigger is activated.",
|
||||
props: {
|
||||
min: 300,
|
||||
max: 2000,
|
||||
step: 100,
|
||||
marks: {
|
||||
300: "0.3s",
|
||||
600: "0.6s",
|
||||
1000: "1s",
|
||||
1500: "1.5s",
|
||||
2000: "2s",
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user