diff --git a/CustomApps/lyrics-plus/OptionsMenu.js b/CustomApps/lyrics-plus/OptionsMenu.js index da365afd73..5934003f56 100644 --- a/CustomApps/lyrics-plus/OptionsMenu.js +++ b/CustomApps/lyrics-plus/OptionsMenu.js @@ -91,11 +91,20 @@ const TranslationMenu = react.memo(({ showTranslationButton, friendlyLanguage, h let translator = new Translator(); - let menuOptions = null; + let sourceOptions = { + none: "None" + }; + const languageOptions = { + off: "Off", + chinese: "Chinese", + japanese: "Japanese", + korean: "Korean" + }; + let modeOptions = {}; switch (friendlyLanguage) { case "japanese": { - menuOptions = { + modeOptions = { furigana: "Furigana", romaji: "Romaji", hiragana: "Hiragana", @@ -104,14 +113,14 @@ const TranslationMenu = react.memo(({ showTranslationButton, friendlyLanguage, h break; } case "korean": { - menuOptions = { + modeOptions = { hangul: "Hangul", romaja: "Romaja" }; break; } case "chinese": { - menuOptions = { + modeOptions = { cn: "Simplified Chinese", hk: "Traditional Chinese (Hong Kong)", tw: "Traditional Chinese (Taiwan)" @@ -120,9 +129,9 @@ const TranslationMenu = react.memo(({ showTranslationButton, friendlyLanguage, h } } if (hasNeteaseTranslation) { - menuOptions = { - ...menuOptions, - neteaseTranslation: "Netease" + sourceOptions = { + ...sourceOptions, + neteaseTranslation: "Netease (Chinese)" }; } @@ -146,10 +155,24 @@ const TranslationMenu = react.memo(({ showTranslationButton, friendlyLanguage, h react.createElement(OptionList, { items: [ { - desc: "Mode", + desc: "Translated Lyrics Provider", + key: `translate:translated-lyrics-source`, + type: ConfigSelection, + options: sourceOptions, + renderInline: true + }, + { + desc: "Detect Language Override", + key: `translate:detect-language-override`, + type: ConfigSelection, + options: languageOptions, + renderInline: true + }, + { + desc: "Text Display Mode", key: `translation-mode:${friendlyLanguage}`, type: ConfigSelection, - options: menuOptions, + options: modeOptions, renderInline: true }, { @@ -167,6 +190,8 @@ const TranslationMenu = react.memo(({ showTranslationButton, friendlyLanguage, h lyricContainerUpdate && lyricContainerUpdate(); CONFIG.visual[name] && Spicetify.showNotification("Translating...", false, 5000); translator.injectExternals(); + // run the translate function after user changes translate lyrics source + // update the dropdowns } }) ), diff --git a/CustomApps/lyrics-plus/ProviderNetease.js b/CustomApps/lyrics-plus/ProviderNetease.js index 3fe19ab703..91c57d8945 100644 --- a/CustomApps/lyrics-plus/ProviderNetease.js +++ b/CustomApps/lyrics-plus/ProviderNetease.js @@ -27,7 +27,7 @@ const ProviderNetease = (function () { "\\s?作?\\s*词|\\s?作?\\s*曲|\\s?编\\s*曲?|\\s?监\\s*制?", ".*编写|.*和音|.*和声|.*合声|.*提琴|.*录|.*工程|.*工作室|.*设计|.*剪辑|.*制作|.*发行|.*出品|.*后期|.*混音|.*缩混", "原唱|翻唱|题字|文案|海报|古筝|二胡|钢琴|吉他|贝斯|笛子|鼓|弦乐", - "lrc|publish|vocal|guitar|program|produce|write" + "lrc|publish|vocal|guitar|program|produce|write|mix" ]; const creditInfoRegExp = new RegExp(`^(${creditInfo.join("|")}).*(:|:)`, "i"); diff --git a/CustomApps/lyrics-plus/Settings.js b/CustomApps/lyrics-plus/Settings.js index 0799a483db..d59011d640 100644 --- a/CustomApps/lyrics-plus/Settings.js +++ b/CustomApps/lyrics-plus/Settings.js @@ -522,8 +522,8 @@ function openConfig() { when: () => !CONFIG.visual["colorful"] }, { - desc: "Text convertion: Chinese-Japanese Detection threshold (Advanced)", - info: "Checks if whenever Hanzi/Kanji or Kana is dominant in lyrics. If the result passes the threshold, it's most likely Japanese, and vice versa. This setting is in percentage.", + desc: "Text convertion: Japanese Detection threshold (Advanced)", + info: "Checks if whenever Kana is dominant in lyrics. If the result passes the threshold, it's most likely Japanese, and vice versa. This setting is in percentage.", key: "ja-detect-threshold", type: ConfigAdjust, min: thresholdSizeLimit.min, diff --git a/CustomApps/lyrics-plus/Utils.js b/CustomApps/lyrics-plus/Utils.js index 1df0414601..a7ff502c5c 100644 --- a/CustomApps/lyrics-plus/Utils.js +++ b/CustomApps/lyrics-plus/Utils.js @@ -41,8 +41,8 @@ class Utils { static removeSongFeat(s) { return ( s - .replace(/-\s+(feat|with).*/i, "") - .replace(/(\(|\[)(feat|with)\.?\s+.*(\)|\])$/i, "") + .replace(/-\s+(feat|with|prod).*/i, "") + .replace(/(\(|\[)(feat|with|prod)\.?\s+.*(\)|\])$/i, "") .trim() || s ); } @@ -57,6 +57,8 @@ class Utils { static detectLanguage(lyrics) { // Should return IETF BCP 47 language tags. + // This should detect the song's main language. + // Remember there is a possibility of a song referencing something in another language and the lyrics show it in that native language! const rawLyrics = lyrics.map(line => line.text).join(" "); const kanaRegex = /[\u3001-\u3003]|[\u3005\u3007]|[\u301d-\u301f]|[\u3021-\u3035]|[\u3038-\u303a]|[\u3040-\u30ff]|[\uff66-\uff9f]/gu; @@ -67,32 +69,22 @@ class Utils { /[萬與醜專業叢東絲丟兩嚴喪個爿豐臨為麗舉麼義烏樂喬習鄉書買亂爭於虧雲亙亞產畝親褻嚲億僅從侖倉儀們價眾優夥會傴傘偉傳傷倀倫傖偽佇體餘傭僉俠侶僥偵側僑儈儕儂俁儔儼倆儷儉債傾傯僂僨償儻儐儲儺兒兌兗黨蘭關興茲養獸囅內岡冊寫軍農塚馮衝決況凍淨淒涼淩減湊凜幾鳳鳧憑凱擊氹鑿芻劃劉則剛創刪別剗剄劊劌剴劑剮劍剝劇勸辦務勱動勵勁勞勢勳猛勩勻匭匱區醫華協單賣盧鹵臥衛卻巹廠廳曆厲壓厭厙廁廂厴廈廚廄廝縣參靉靆雙發變敘疊葉號歎嘰籲後嚇呂嗎唚噸聽啟吳嘸囈嘔嚦唄員咼嗆嗚詠哢嚨嚀噝吒噅鹹呱響啞噠嘵嗶噦嘩噲嚌噥喲嘜嗊嘮啢嗩唕喚呼嘖嗇囀齧囉嘽嘯噴嘍嚳囁嗬噯噓嚶囑嚕劈囂謔團園囪圍圇國圖圓聖壙場阪壞塊堅壇壢壩塢墳墜壟壟壚壘墾坰堊墊埡墶壋塏堖塒塤堝墊垵塹墮壪牆壯聲殼壺壼處備複夠頭誇夾奪奩奐奮獎奧妝婦媽嫵嫗媯姍薑婁婭嬈嬌孌娛媧嫻嫿嬰嬋嬸媼嬡嬪嬙嬤孫學孿寧寶實寵審憲宮寬賓寢對尋導壽將爾塵堯尷屍盡層屭屜屆屬屢屨嶼歲豈嶇崗峴嶴嵐島嶺嶽崠巋嶨嶧峽嶢嶠崢巒嶗崍嶮嶄嶸嶔崳嶁脊巔鞏巰幣帥師幃帳簾幟帶幀幫幬幘幗冪襆幹並廣莊慶廬廡庫應廟龐廢廎廩開異棄張彌弳彎彈強歸當錄彠彥徹徑徠禦憶懺憂愾懷態慫憮慪悵愴憐總懟懌戀懇惡慟懨愷惻惱惲悅愨懸慳憫驚懼慘懲憊愜慚憚慣湣慍憤憒願懾憖怵懣懶懍戇戔戲戧戰戩戶紮撲扡執擴捫掃揚擾撫拋摶摳掄搶護報擔擬攏揀擁攔擰撥擇掛摯攣掗撾撻挾撓擋撟掙擠揮撏撈損撿換搗據撚擄摑擲撣摻摜摣攬撳攙擱摟攪攜攝攄擺搖擯攤攖撐攆擷擼攛擻攢敵斂數齋斕鬥斬斷無舊時曠暘曇晝曨顯晉曬曉曄暈暉暫曖劄術樸機殺雜權條來楊榪傑極構樅樞棗櫪梘棖槍楓梟櫃檸檉梔柵標棧櫛櫳棟櫨櫟欄樹棲樣欒棬椏橈楨檔榿橋樺檜槳樁夢檮棶檢欞槨櫝槧欏橢樓欖櫬櫚櫸檟檻檳櫧橫檣櫻櫫櫥櫓櫞簷檁歡歟歐殲歿殤殘殞殮殫殯毆毀轂畢斃氈毿氌氣氫氬氳彙漢汙湯洶遝溝沒灃漚瀝淪滄渢溈滬濔濘淚澩瀧瀘濼瀉潑澤涇潔灑窪浹淺漿澆湞溮濁測澮濟瀏滻渾滸濃潯濜塗湧濤澇淶漣潿渦溳渙滌潤澗漲澀澱淵淥漬瀆漸澠漁瀋滲溫遊灣濕潰濺漵漊潷滾滯灩灄滿瀅濾濫灤濱灘澦濫瀠瀟瀲濰潛瀦瀾瀨瀕灝滅燈靈災燦煬爐燉煒熗點煉熾爍爛烴燭煙煩燒燁燴燙燼熱煥燜燾煆糊溜愛爺牘犛牽犧犢強狀獷獁猶狽麅獮獰獨狹獅獪猙獄猻獫獵獼玀豬貓蝟獻獺璣璵瑒瑪瑋環現瑲璽瑉玨琺瓏璫琿璡璉瑣瓊瑤璦璿瓔瓚甕甌電畫暢佘疇癤療瘧癘瘍鬁瘡瘋皰屙癰痙癢瘂癆瘓癇癡癉瘮瘞瘺癟癱癮癭癩癬癲臒皚皺皸盞鹽監蓋盜盤瞘眥矓著睜睞瞼瞞矚矯磯礬礦碭碼磚硨硯碸礪礱礫礎硜矽碩硤磽磑礄確鹼礙磧磣堿镟滾禮禕禰禎禱禍稟祿禪離禿稈種積稱穢穠穭稅穌穩穡窮竊竅窯竄窩窺竇窶豎競篤筍筆筧箋籠籩築篳篩簹箏籌簽簡籙簀篋籜籮簞簫簣簍籃籬籪籟糴類秈糶糲粵糞糧糝餱緊縶糸糾紆紅紂纖紇約級紈纊紀紉緯紜紘純紕紗綱納紝縱綸紛紙紋紡紵紖紐紓線紺絏紱練組紳細織終縐絆紼絀紹繹經紿綁絨結絝繞絰絎繪給絢絳絡絕絞統綆綃絹繡綌綏絛繼綈績緒綾緓續綺緋綽緔緄繩維綿綬繃綢綯綹綣綜綻綰綠綴緇緙緗緘緬纜緹緲緝縕繢緦綞緞緶線緱縋緩締縷編緡緣縉縛縟縝縫縗縞纏縭縊縑繽縹縵縲纓縮繆繅纈繚繕繒韁繾繰繯繳纘罌網羅罰罷羆羈羥羨翹翽翬耮耬聳恥聶聾職聹聯聵聰肅腸膚膁腎腫脹脅膽勝朧腖臚脛膠脈膾髒臍腦膿臠腳脫腡臉臘醃膕齶膩靦膃騰臏臢輿艤艦艙艫艱豔艸藝節羋薌蕪蘆蓯葦藶莧萇蒼苧蘇檾蘋莖蘢蔦塋煢繭荊薦薘莢蕘蓽蕎薈薺蕩榮葷滎犖熒蕁藎蓀蔭蕒葒葤藥蒞蓧萊蓮蒔萵薟獲蕕瑩鶯蓴蘀蘿螢營縈蕭薩蔥蕆蕢蔣蔞藍薊蘺蕷鎣驀薔蘞藺藹蘄蘊藪槁蘚虜慮虛蟲虯蟣雖蝦蠆蝕蟻螞蠶蠔蜆蠱蠣蟶蠻蟄蛺蟯螄蠐蛻蝸蠟蠅蟈蟬蠍螻蠑螿蟎蠨釁銜補襯袞襖嫋褘襪襲襏裝襠褌褳襝褲襇褸襤繈襴見觀覎規覓視覘覽覺覬覡覿覥覦覯覲覷觴觸觶讋譽謄訁計訂訃認譏訐訌討讓訕訖訓議訊記訒講諱謳詎訝訥許訛論訩訟諷設訪訣證詁訶評詛識詗詐訴診詆謅詞詘詔詖譯詒誆誄試詿詩詰詼誠誅詵話誕詬詮詭詢詣諍該詳詫諢詡譸誡誣語誚誤誥誘誨誑說誦誒請諸諏諾讀諑誹課諉諛誰諗調諂諒諄誶談誼謀諶諜謊諫諧謔謁謂諤諭諼讒諮諳諺諦謎諞諝謨讜謖謝謠謗諡謙謐謹謾謫譾謬譚譖譙讕譜譎讞譴譫讖穀豶貝貞負貟貢財責賢敗賬貨質販貪貧貶購貯貫貳賤賁貰貼貴貺貸貿費賀貽賊贄賈賄貲賃賂贓資賅贐賕賑賚賒賦賭齎贖賞賜贔賙賡賠賧賴賵贅賻賺賽賾贗讚贇贈贍贏贛赬趙趕趨趲躉躍蹌蹠躒踐躂蹺蹕躚躋踴躊蹤躓躑躡蹣躕躥躪躦軀車軋軌軒軑軔轉軛輪軟轟軲軻轤軸軹軼軤軫轢軺輕軾載輊轎輈輇輅較輒輔輛輦輩輝輥輞輬輟輜輳輻輯轀輸轡轅轄輾轆轍轔辭辯辮邊遼達遷過邁運還這進遠違連遲邇逕跡適選遜遞邐邏遺遙鄧鄺鄔郵鄒鄴鄰鬱郤郟鄶鄭鄆酈鄖鄲醞醱醬釅釃釀釋裏钜鑒鑾鏨釓釔針釘釗釙釕釷釺釧釤鈒釩釣鍆釹鍚釵鈃鈣鈈鈦鈍鈔鍾鈉鋇鋼鈑鈐鑰欽鈞鎢鉤鈧鈁鈥鈄鈕鈀鈺錢鉦鉗鈷缽鈳鉕鈽鈸鉞鑽鉬鉭鉀鈿鈾鐵鉑鈴鑠鉛鉚鈰鉉鉈鉍鈹鐸鉶銬銠鉺銪鋏鋣鐃銍鐺銅鋁銱銦鎧鍘銖銑鋌銩銛鏵銓鉿銚鉻銘錚銫鉸銥鏟銃鐋銨銀銣鑄鐒鋪鋙錸鋱鏈鏗銷鎖鋰鋥鋤鍋鋯鋨鏽銼鋝鋒鋅鋶鐦鐧銳銻鋃鋟鋦錒錆鍺錯錨錡錁錕錩錫錮鑼錘錐錦鍁錈錇錟錠鍵鋸錳錙鍥鍈鍇鏘鍶鍔鍤鍬鍾鍛鎪鍠鍰鎄鍍鎂鏤鎡鏌鎮鎛鎘鑷鐫鎳鎿鎦鎬鎊鎰鎔鏢鏜鏍鏰鏞鏡鏑鏃鏇鏐鐔钁鐐鏷鑥鐓鑭鐠鑹鏹鐙鑊鐳鐶鐲鐮鐿鑔鑣鑞鑲長門閂閃閆閈閉問闖閏闈閑閎間閔閌悶閘鬧閨聞闥閩閭闓閥閣閡閫鬮閱閬闍閾閹閶鬩閿閽閻閼闡闌闃闠闊闋闔闐闒闕闞闤隊陽陰陣階際陸隴陳陘陝隉隕險隨隱隸雋難雛讎靂霧霽黴靄靚靜靨韃鞽韉韝韋韌韍韓韙韞韜韻頁頂頃頇項順須頊頑顧頓頎頒頌頏預顱領頗頸頡頰頲頜潁熲頦頤頻頮頹頷頴穎顆題顒顎顓顏額顳顢顛顙顥纇顫顬顰顴風颺颭颮颯颶颸颼颻飀飄飆飆飛饗饜飣饑飥餳飩餼飪飫飭飯飲餞飾飽飼飿飴餌饒餉餄餎餃餏餅餑餖餓餘餒餕餜餛餡館餷饋餶餿饞饁饃餺餾饈饉饅饊饌饢馬馭馱馴馳驅馹駁驢駔駛駟駙駒騶駐駝駑駕驛駘驍罵駰驕驊駱駭駢驫驪騁驗騂駸駿騏騎騍騅騌驌驂騙騭騤騷騖驁騮騫騸驃騾驄驏驟驥驦驤髏髖髕鬢魘魎魚魛魢魷魨魯魴魺鮁鮃鯰鱸鮋鮓鮒鮊鮑鱟鮍鮐鮭鮚鮳鮪鮞鮦鰂鮜鱠鱭鮫鮮鮺鯗鱘鯁鱺鰱鰹鯉鰣鰷鯀鯊鯇鮶鯽鯒鯖鯪鯕鯫鯡鯤鯧鯝鯢鯰鯛鯨鯵鯴鯔鱝鰈鰏鱨鯷鰮鰃鰓鱷鰍鰒鰉鰁鱂鯿鰠鼇鰭鰨鰥鰩鰟鰜鰳鰾鱈鱉鰻鰵鱅鰼鱖鱔鱗鱒鱯鱤鱧鱣鳥鳩雞鳶鳴鳲鷗鴉鶬鴇鴆鴣鶇鸕鴨鴞鴦鴒鴟鴝鴛鴬鴕鷥鷙鴯鴰鵂鴴鵃鴿鸞鴻鵐鵓鸝鵑鵠鵝鵒鷳鵜鵡鵲鶓鵪鶤鵯鵬鵮鶉鶊鵷鷫鶘鶡鶚鶻鶿鶥鶩鷊鷂鶲鶹鶺鷁鶼鶴鷖鸚鷓鷚鷯鷦鷲鷸鷺鸇鷹鸌鸏鸛鸘鹺麥麩黃黌黶黷黲黽黿鼂鼉鞀鼴齇齊齏齒齔齕齗齟齡齙齠齜齦齬齪齲齷龍龔龕龜誌製谘隻裡係範鬆冇嚐嘗鬨麵準鐘彆閒乾儘臟拚]/gu; const cjkMatch = rawLyrics.match(new RegExp(kanaRegex.source + "|" + /\p{Unified_Ideograph}/gu.source + "|" + hangulRegex.source, "gu")); - let kanaCount = 0; - let tradCount = 0; - let simpCount = 0; if (!cjkMatch) return; - for (const character of cjkMatch) { - if (character.match(hangulRegex)) { - return "ko"; - } - if (character.match(kanaRegex)) { - kanaCount++; - } - if (character.match(simpRegex)) { - simpCount++; - } - if (character.match(tradRegex)) { - tradCount++; - } - } + const kanaCount = cjkMatch.filter(glyph => kanaRegex.test(glyph)).length; + const simpCount = cjkMatch.filter(glyph => simpRegex.test(glyph)).length; + const tradCount = cjkMatch.filter(glyph => tradRegex.test(glyph)).length; const kanaPercentage = kanaCount / cjkMatch.length; const simpPercentage = simpCount / cjkMatch.length; const tradPercentage = tradCount / cjkMatch.length; - if (((kanaPercentage - (simpPercentage + tradPercentage) + 1) / 2) * 100 >= CONFIG.visual["ja-detect-threshold"]) { + if (cjkMatch.filter(glyph => hangulRegex.test(glyph))) { + return "ko"; + } + + if (((kanaPercentage - (1 - kanaPercentage) + 1) / 2) * 100 >= CONFIG.visual["ja-detect-threshold"]) { return "ja"; } diff --git a/CustomApps/lyrics-plus/index.js b/CustomApps/lyrics-plus/index.js index 0b3de125e7..be077da25f 100644 --- a/CustomApps/lyrics-plus/index.js +++ b/CustomApps/lyrics-plus/index.js @@ -47,6 +47,8 @@ const CONFIG = { ["lines-before"]: localStorage.getItem("lyrics-plus:visual:lines-before") || "0", ["lines-after"]: localStorage.getItem("lyrics-plus:visual:lines-after") || "2", ["font-size"]: localStorage.getItem("lyrics-plus:visual:font-size") || "32", + ["translate:translated-lyrics-source"]: localStorage.getItem("lyrics-plus:visual:translate:translated-lyrics-source") || "none", + ["translate:detect-language-override"]: localStorage.getItem("lyrics-plus:visual:translate:detect-language-override") || "off", ["translation-mode:japanese"]: localStorage.getItem("lyrics-plus:visual:translation-mode:japanese") || "furigana", ["translation-mode:korean"]: localStorage.getItem("lyrics-plus:visual:translation-mode:korean") || "hangul", ["translation-mode:chinese"]: localStorage.getItem("lyrics-plus:visual:translation-mode:chinese") || "cn", @@ -339,13 +341,23 @@ class LyricsContainer extends react.Component { this.translateLyrics(); } + lyricsSource(state) { + switch (CONFIG.visual["translate:translated-lyrics-source"]) { + case "neteaseTranslation": { + if (this.state.neteaseTranslation !== null) return this.state.neteaseTranslation; + break; + } + } + return state; + } + async translateLyrics() { if (!this.translator || !this.translator.finished) { setTimeout(this.translateLyrics.bind(this), 100); return; } - const lyricsToTranslate = this.state.synced ?? this.state.unsynced; + const lyricsToTranslate = this.lyricsSource(this.state.synced ?? this.state.unsynced); if (!lyricsToTranslate) return; @@ -589,6 +601,14 @@ class LyricsContainer extends react.Component { this.mousetrap.bind(CONFIG.visual["fullscreen-key"], this.toggleFullscreen); } + componentDidUpdate(prevProps, prevState) { + console.log(this.state); + console.log(prevState); + if (CONFIG.visual["translate:translated-lyrics-source"] !== "none" && this.state !== prevState) { + this.translateLyrics(); + } + } + render() { const fadLyricsContainer = document.getElementById("fad-lyrics-plus-container"); this.state.isFADMode = !!fadLyricsContainer; @@ -632,9 +652,16 @@ class LyricsContainer extends react.Component { } const hasNeteaseTranslation = this.state.neteaseTranslation !== null; - const language = (this.state.synced || this.state.unsynced) && Utils.detectLanguage(this.state.synced || this.state.unsynced); + const language = () => { + if (!this.state.synced || !this.state.unsynced) return; + if ([CONFIG.visual["translate:detect-language-override"]] == "off") { + return Utils.detectLanguage(this.lyricsSource(this.state.synced || this.state.unsynced)); + } + return CONFIG.visual["translate:detect-language-override"]; + }; + console.log(language()); const languageDisplayNames = new Intl.DisplayNames(["en"], { type: "language" }); - const friendlyLanguage = language && languageDisplayNames.of(language.split("-")[0]).toLowerCase(); + const friendlyLanguage = language() && languageDisplayNames.of(language().split("-")[0]).toLowerCase(); const showTranslationButton = (friendlyLanguage || hasNeteaseTranslation) && (mode == SYNCED || mode == UNSYNCED); const translatedLyrics = this.state[CONFIG.visual[`translation-mode:${friendlyLanguage}`]]; @@ -652,14 +679,14 @@ class LyricsContainer extends react.Component { } else if (mode === SYNCED && this.state.synced) { activeItem = react.createElement(CONFIG.visual["synced-compact"] ? SyncedLyricsPage : SyncedExpandedLyricsPage, { trackUri: this.state.uri, - lyrics: CONFIG.visual["translate"] && translatedLyrics ? translatedLyrics : this.state.synced, + lyrics: CONFIG.visual["translate"] && translatedLyrics ? translatedLyrics : this.lyricsSource(this.state.synced), provider: this.state.provider, copyright: this.state.copyright }); } else if (mode === UNSYNCED && this.state.unsynced) { activeItem = react.createElement(UnsyncedLyricsPage, { trackUri: this.state.uri, - lyrics: CONFIG.visual["translate"] && translatedLyrics ? translatedLyrics : this.state.unsynced, + lyrics: CONFIG.visual["translate"] && translatedLyrics ? translatedLyrics : this.lyricsSource(this.state.unsynced), provider: this.state.provider, copyright: this.state.copyright }); diff --git a/CustomApps/new-releases/Card.js b/CustomApps/new-releases/Card.js index 7db04857d5..31f4666289 100644 --- a/CustomApps/new-releases/Card.js +++ b/CustomApps/new-releases/Card.js @@ -19,6 +19,12 @@ class Card extends react.Component { event.stopPropagation(); } + closeButtonClicked(event) { + removeCards(this.props.uri); + Spicetify.showNotification(`Dismissed ${this.title} from ${this.artist.name}`); + event.stopPropagation(); + } + render() { let detail = []; this.visual.type && detail.push(this.type); @@ -44,7 +50,7 @@ class Card extends react.Component { "div", { className: "main-card-draggable", - draggable: "true" + draggable: "false" }, react.createElement( "div", @@ -104,6 +110,32 @@ class Card extends react.Component { ) ) ) + ), + react.createElement( + Spicetify.ReactComponent.TooltipWrapper, + { label: "Dismiss" }, + react.createElement( + "button", + { + className: "main-card-closeButton", + onClick: this.closeButtonClicked.bind(this) + }, + react.createElement( + "svg", + { + width: "18", + height: "18", + viewBox: "0 0 32 32", + xmlns: "http://www.w3.org/2000/svg", + className: "main-card-closeButton-svg" + }, + react.createElement("path", { + d: "M31.098 29.794L16.955 15.65 31.097 1.51 29.683.093 15.54 14.237 1.4.094-.016 1.508 14.126 15.65-.016 29.795l1.414 1.414L15.54 17.065l14.144 14.143", + fill: "var(--spice-text)", + fillRule: "evenodd" + }) + ) + ) ) ), react.createElement( diff --git a/CustomApps/new-releases/Settings.js b/CustomApps/new-releases/Settings.js index b66a734448..95b603b43f 100644 --- a/CustomApps/new-releases/Settings.js +++ b/CustomApps/new-releases/Settings.js @@ -269,7 +269,30 @@ function openConfig() { } localStorage.setItem(`${APP_NAME}:${name}`, value); } - }) + }), + react.createElement( + "div", + { + className: "setting-row" + }, + react.createElement( + "label", + { + className: "col description" + }, + "Dismissed releases" + ), + react.createElement( + "div", + { + className: "col action" + }, + react.createElement(ButtonText, { + text: Spicetify.Locale.get("equalizer.reset"), + onClick: removeCards.bind(this, null, "reset") + }) + ) + ) ); Spicetify.PopupModal.display({ diff --git a/CustomApps/new-releases/index.js b/CustomApps/new-releases/index.js index 779ee1fd2d..0959bd16ce 100644 --- a/CustomApps/new-releases/index.js +++ b/CustomApps/new-releases/index.js @@ -40,10 +40,19 @@ const CONFIG = { relative: getConfig("new-releases:relative", false) }; +let dismissed; +try { + dismissed = JSON.parse(Spicetify.LocalStorage.get("new-releases:dismissed")); + if (!Array.isArray(dismissed)) throw ""; +} catch { + dismissed = []; +} + let gridList = []; let lastScroll = 0; let gridUpdatePostsVisual; +let removeCards; let today = new Date(); CONFIG.range = parseInt(CONFIG.range) || 30; @@ -74,6 +83,8 @@ class Grid extends react.Component { updatePostsVisual() { gridList = []; for (const date of dateList) { + if (separatedByDate[date].every(card => dismissed.includes(card.props.uri))) continue; + gridList.push( react.createElement( "div", @@ -93,13 +104,32 @@ class Grid extends react.Component { "--grid-gap": "24px" } }, - separatedByDate[date].map(card => react.createElement(Card, card.props)) + separatedByDate[date].map(card => !dismissed.includes(card.props.uri) && react.createElement(Card, card.props)) ) ); } this.setState({ cards: [...gridList] }); } + removeCards(id, type) { + switch (type) { + case "reset": + Spicetify.showNotification("Reset dismissed releases"); + dismissed = []; + break; + case "undo": + if (!dismissed[0]) Spicetify.showNotification("Nothing to undo", true); + else Spicetify.showNotification("Undone last dismiss"); + dismissed.pop(); + break; + default: + dismissed.push(id); + break; + } + Spicetify.LocalStorage.set("new-releases:dismissed", JSON.stringify(dismissed)); + this.updatePostsVisual(); + } + async reload() { gridList = []; separatedByDate = {}; @@ -146,6 +176,8 @@ class Grid extends react.Component { } for (const date of dateList) { + if (separatedByDate[date].every(card => dismissed.includes(card.props.uri))) continue; + gridList.push( react.createElement( "div", @@ -165,7 +197,7 @@ class Grid extends react.Component { "--grid-gap": "24px" } }, - separatedByDate[date] + separatedByDate[date].filter(card => !dismissed.includes(card.props.uri)) ) ); } @@ -175,6 +207,7 @@ class Grid extends react.Component { async componentDidMount() { gridUpdatePostsVisual = this.updatePostsVisual.bind(this); + removeCards = this.removeCards.bind(this); this.configButton = new Spicetify.Menu.Item( "New Releases config", @@ -223,6 +256,10 @@ class Grid extends react.Component { react.createElement(ButtonText, { text: Spicetify.Locale.get("playlist.extender.refresh"), onClick: this.reload.bind(this) + }), + react.createElement(ButtonText, { + text: "undo", // no locale for this + onClick: this.removeCards.bind(this, null, "undo") }) ) ), diff --git a/CustomApps/new-releases/style.css b/CustomApps/new-releases/style.css index 08d0be9924..e271e907c4 100644 --- a/CustomApps/new-releases/style.css +++ b/CustomApps/new-releases/style.css @@ -125,3 +125,39 @@ option { .new-releases-header + .main-gridContainer-gridContainer .main-card-card .main-playButton-PlayButton span:hover { transform: scale(1.04); } + +.main-card-closeButton { + border: none; + -webkit-tap-highlight-color: transparent; + background-color: rgba(var(--spice-rgb-shadow), 0.7); + color: var(--spice-sidebar); + display: flex; + align-items: center; + justify-content: center; + inline-size: 32px; + block-size: 32px; + border-radius: calc(var(--card-container-border-radius) + 2px); + position: absolute !important; + top: 10px; + right: 10px; +} + +.main-card-closeButton { + visibility: hidden; + opacity: 0; + transition: visibility 0s, opacity 0.3s ease; +} + +.main-card-closeButton:active { + transform: scale(0.96); +} +.main-card-closeButton:hover { + background-color: rgba(var(--spice-rgb-shadow), 1); + transition: background-color 0s, opacity 1s ease; +} + +.main-card-card:hover .main-card-closeButton { + visibility: visible; + opacity: 1; + transition: visibility 0s, opacity 0.3s ease; +} diff --git a/Extensions/fullAppDisplay.js b/Extensions/fullAppDisplay.js index 0c8696fd51..fe8ff90b41 100644 --- a/Extensions/fullAppDisplay.js +++ b/Extensions/fullAppDisplay.js @@ -365,9 +365,11 @@ body.video-full-screen.video-full-screen--hide-ui { const albumInfo = await Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/albums/${id}`); const albumDate = new Date(albumInfo.release_date); - const recentDate = new Date(); - recentDate.setMonth(recentDate.getMonth() - 6); - return albumDate.toLocaleString("default", albumDate > recentDate ? { year: "numeric", month: "short" } : { year: "numeric" }); + return albumDate.toLocaleString("default", { + year: "numeric", + month: "short", + day: "numeric" + }); } async fetchInfo() { diff --git a/Extensions/popupLyrics.js b/Extensions/popupLyrics.js index f123d71e18..6a5307b54a 100644 --- a/Extensions/popupLyrics.js +++ b/Extensions/popupLyrics.js @@ -65,8 +65,8 @@ function PopupLyrics() { static removeExtraInfo(s) { return ( s - .replace(/-\s+(feat|with).*/i, "") - .replace(/(\(|\[)(feat|with)\.?\s+.*(\)|\])$/i, "") + .replace(/-\s+(feat|with|prod).*/i, "") + .replace(/(\(|\[)(feat|with|prod)\.?\s+.*(\)|\])$/i, "") .replace(/\s-\s.*/, "") .trim() || s ); @@ -146,7 +146,7 @@ function PopupLyrics() { const subtitle = body["track.subtitles.get"].message.body.subtitle_list[0].subtitle; const lyrics = JSON.parse(subtitle.subtitle_body).map(line => ({ - text: line.text || "⋯", + text: line.text || "♪", startTime: line.time.total })); return { lyrics }; @@ -187,10 +187,10 @@ function PopupLyrics() { lyricStr = lyricStr.lyric; const otherInfoKeys = [ - "作?\\s*词|作?\\s*曲|编\\s*曲?|监\\s*制?", + "\\s?作?\\s*词|\\s?作?\\s*曲|\\s?编\\s*曲?|\\s?监\\s*制?", ".*编写|.*和音|.*和声|.*合声|.*提琴|.*录|.*工程|.*工作室|.*设计|.*剪辑|.*制作|.*发行|.*出品|.*后期|.*混音|.*缩混", "原唱|翻唱|题字|文案|海报|古筝|二胡|钢琴|吉他|贝斯|笛子|鼓|弦乐", - "lrc|publish|vocal|guitar|program|produce|write" + "lrc|publish|vocal|guitar|program|produce|write|mix" ]; const otherInfoRegexp = new RegExp(`^(${otherInfoKeys.join("|")}).*(:|:)`, "i"); @@ -219,11 +219,12 @@ function PopupLyrics() { const matchResult = slice.match(/[^\[\]]+/g); const [key, value] = matchResult[0].split(":") || []; const [min, sec] = [parseFloat(key), parseFloat(value)]; - if (!isNaN(min) && !otherInfoRegexp.test(text)) { + if (!isNaN(min) && !isNaN(sec) && !otherInfoRegexp.test(text)) { result.startTime = min * 60 + sec; result.text = text || "♪"; + return result; } - return result; + return; }); }) .flat() diff --git a/css-map.json b/css-map.json index 13a7a8124a..e197596f6f 100644 --- a/css-map.json +++ b/css-map.json @@ -2325,5 +2325,8 @@ "aPIPHZU8F7TMi6LEw_Yq": "x-settings-equalizerPanelGainLabelDown", "j7cPtD65ArW8eWnGNrUo": "x-settings-equalizerPresetsContainer", "ZzNb8P1Bz1VHtlhimIWM": "x-settings-equalizerPresetsLabel", - "FulR95cAh4QPdw6wUeRw": "x-settings-equalizerWrapper" + "FulR95cAh4QPdw6wUeRw": "x-settings-equalizerWrapper", + "ZQftYELq0aOsg6tPbVbV": "Root__top-container", + "WIPpgUp9J37Dwd0ZJnv0": "Root__top-container--right-sidebar-hidden", + "H1bRFdpa3qfekTVTeDwC": "Root__top-container--has-notice-bar" } diff --git a/go.mod b/go.mod index aec4c4a313..da6a960aba 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,11 @@ go 1.19 require ( github.com/go-ini/ini v1.67.0 github.com/mattn/go-colorable v0.1.13 - golang.org/x/net v0.9.0 + golang.org/x/net v0.10.0 ) require ( github.com/mattn/go-isatty v0.0.16 // indirect github.com/stretchr/testify v1.7.1 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/sys v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index d974f5df19..9987cfa8ae 100644 --- a/go.sum +++ b/go.sum @@ -11,11 +11,11 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/jsHelper/spicetifyWrapper.js b/jsHelper/spicetifyWrapper.js index 5f5678aa8d..f65bde7e37 100644 --- a/jsHelper/spicetifyWrapper.js +++ b/jsHelper/spicetifyWrapper.js @@ -1403,11 +1403,7 @@ Spicetify.Playbar = (function() { this.onClick = onClick; this.disabled = disabled; this.active = active; - Array.from(sibling?.classList ?? []).forEach((className) => { - if (!className.startsWith("main-genericButton")) { - this.element.classList.add(className); - } - }); + addClassname(this.element); this.tippy = Spicetify.Tippy?.(this.element, { content: label, ...Spicetify.TippyProps, @@ -1447,7 +1443,7 @@ Spicetify.Playbar = (function() { get active() { return this._active; } register() { buttonsStash.add(this.element); - rightContainer?.prepend(...buttonsStash); + rightContainer?.prepend(this.element); } deregister() { buttonsStash.delete(this.element); @@ -1456,22 +1452,26 @@ Spicetify.Playbar = (function() { } (function waitForPlaybarMounted() { - sibling = document.querySelector(".main-nowPlayingBar-right .main-genericButton-button"); rightContainer = document.querySelector(".main-nowPlayingBar-right > div"); if (!rightContainer) { setTimeout(waitForPlaybarMounted, 300); return; } - Array.from(sibling?.classList ?? []).forEach((className) => { - if (!className.startsWith("main-genericButton")) { - buttonsStash.forEach((button) => { - button.classList.add(className); - }); - } - }); + buttonsStash.forEach((button) => addClassname(button)); rightContainer.prepend(...buttonsStash); })(); + function addClassname(element) { + sibling = document.querySelector(".main-nowPlayingBar-right .main-genericButton-button"); + if (!sibling) { + setTimeout(addClassname, 300, element); + return; + } + Array.from(sibling.classList).forEach((className) => { + if (!className.startsWith("main-genericButton")) element.classList.add(className); + }); + } + return { Button }; })(); @@ -1633,7 +1633,7 @@ Spicetify.Playbar = (function() { ); } } catch (err) { - console.err(err); + console.error(err); } })();