From aafd14be318044a916ca3e00ad520bb010b902ee Mon Sep 17 00:00:00 2001 From: Ignacio Date: Sat, 6 Jul 2024 21:00:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=87=E6=AD=8C=E6=9B=B2=E7=A7=BB=E5=88=B0?= =?UTF-8?q?=E5=BE=8C=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/05_anki_unique_front/down.sql | 1 + migrations/05_anki_unique_front/up.sql | 1 + migrations/06_songs/down.sql | 1 + migrations/06_songs/up.sql | 10 + public/locales/en/translation.json | 3 + public/locales/es/translation.json | 3 + public/locales/jp/translation.json | 3 + public/locales/zh_hant/translation.json | 3 + src/backend/db/anki.rs | 13 +- src/backend/db/mod.rs | 3 +- src/backend/db/models.rs | 12 +- src/backend/db/schema.rs | 12 ++ src/backend/db/song.rs | 70 +++++++ src/backend/gql/mod.rs | 35 +++- src/backend/gql/models.rs | 25 ++- src/backend/mod.rs | 3 +- .../containers/AnkisSection/AnkisSection.tsx | 64 ++++--- src/react-ui/containers/Panel/Panel.tsx | 1 + .../RecordsSection/RecordsSection.tsx | 51 ++++- .../RecordsSection/screens/RecordsList.tsx | 180 +++++++++--------- src/react-ui/graphql/graphql.ts | 29 +++ .../languages/cantonese/songs/index.ts | 73 ------- .../songs/janice-vidal-jat-gaak-gaak.ts | 17 -- .../cantonese/songs/kay-tse-faat-jyu-cing.ts | 21 -- .../cantonese/songs/kay-tse-lei-bat-hoi.ts | 17 -- .../songs/kay-tse-nei-mun-dik-hang-fuk.ts | 29 --- .../cantonese/songs/kay-tse-nin-dou-zi-go.ts | 28 --- .../cantonese/songs/kay-tse-saan-lam-dou.ts | 25 --- .../cantonese/songs/kay-tse-zoi-ngo-zau.ts | 27 --- .../songs/my-little-airport-ci-go-mou-gaai.ts | 9 - ...my-little-airport-heoi-seon-wo-maai-dip.ts | 9 - ...my-little-airport-naa-zan-si-bat-zi-dou.ts | 11 -- ...n-hing-dik-caa-cang-ting-lou-baan-loeng.ts | 19 -- .../my-little-airport-zoi-saat-jat-go-jan.ts | 13 -- .../my-little-airport_jat-go-ngaan-san.ts | 17 -- .../songs/stephy-tang_din-dang-daam.ts | 15 -- src/react-ui/languages/common/conversion.ts | 3 + .../languages/mandarin/songs/index.ts | 31 --- .../mandarin/songs/kay-tse_di-er-ge-jia.ts | 17 -- .../songs/sandy-lam_zhi-shao-hai-you-ni.ts | 37 ---- .../mandarin/songs/stefanie-sun_yu-jian.ts | 15 -- src/react-ui/lib/backendClient.ts | 57 +++++- 42 files changed, 455 insertions(+), 558 deletions(-) create mode 100644 migrations/05_anki_unique_front/down.sql create mode 100644 migrations/05_anki_unique_front/up.sql create mode 100644 migrations/06_songs/down.sql create mode 100644 migrations/06_songs/up.sql create mode 100644 src/backend/db/song.rs delete mode 100644 src/react-ui/languages/cantonese/songs/index.ts delete mode 100644 src/react-ui/languages/cantonese/songs/janice-vidal-jat-gaak-gaak.ts delete mode 100644 src/react-ui/languages/cantonese/songs/kay-tse-faat-jyu-cing.ts delete mode 100644 src/react-ui/languages/cantonese/songs/kay-tse-lei-bat-hoi.ts delete mode 100644 src/react-ui/languages/cantonese/songs/kay-tse-nei-mun-dik-hang-fuk.ts delete mode 100644 src/react-ui/languages/cantonese/songs/kay-tse-nin-dou-zi-go.ts delete mode 100644 src/react-ui/languages/cantonese/songs/kay-tse-saan-lam-dou.ts delete mode 100644 src/react-ui/languages/cantonese/songs/kay-tse-zoi-ngo-zau.ts delete mode 100644 src/react-ui/languages/cantonese/songs/my-little-airport-ci-go-mou-gaai.ts delete mode 100644 src/react-ui/languages/cantonese/songs/my-little-airport-heoi-seon-wo-maai-dip.ts delete mode 100644 src/react-ui/languages/cantonese/songs/my-little-airport-naa-zan-si-bat-zi-dou.ts delete mode 100644 src/react-ui/languages/cantonese/songs/my-little-airport-nin-hing-dik-caa-cang-ting-lou-baan-loeng.ts delete mode 100644 src/react-ui/languages/cantonese/songs/my-little-airport-zoi-saat-jat-go-jan.ts delete mode 100644 src/react-ui/languages/cantonese/songs/my-little-airport_jat-go-ngaan-san.ts delete mode 100644 src/react-ui/languages/cantonese/songs/stephy-tang_din-dang-daam.ts delete mode 100644 src/react-ui/languages/mandarin/songs/index.ts delete mode 100644 src/react-ui/languages/mandarin/songs/kay-tse_di-er-ge-jia.ts delete mode 100644 src/react-ui/languages/mandarin/songs/sandy-lam_zhi-shao-hai-you-ni.ts delete mode 100644 src/react-ui/languages/mandarin/songs/stefanie-sun_yu-jian.ts diff --git a/migrations/05_anki_unique_front/down.sql b/migrations/05_anki_unique_front/down.sql new file mode 100644 index 00000000..3ac886eb --- /dev/null +++ b/migrations/05_anki_unique_front/down.sql @@ -0,0 +1 @@ +DROP INDEX unique_front_user_id; diff --git a/migrations/05_anki_unique_front/up.sql b/migrations/05_anki_unique_front/up.sql new file mode 100644 index 00000000..94f675cd --- /dev/null +++ b/migrations/05_anki_unique_front/up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX unique_front_user_id ON ankis(front, user_id); diff --git a/migrations/06_songs/down.sql b/migrations/06_songs/down.sql new file mode 100644 index 00000000..1cc19856 --- /dev/null +++ b/migrations/06_songs/down.sql @@ -0,0 +1 @@ +DROP TABLE songs; diff --git a/migrations/06_songs/up.sql b/migrations/06_songs/up.sql new file mode 100644 index 00000000..543da3f3 --- /dev/null +++ b/migrations/06_songs/up.sql @@ -0,0 +1,10 @@ +CREATE TABLE songs ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + title VARCHAR(255) NOT NULL, + artist VARCHAR(255) NOT NULL, + language VARCHAR(255) NOT NULL, + video_url VARCHAR(255) NOT NULL, + lyrics TEXT NOT NULL +); + +CREATE UNIQUE INDEX songs_title_artist_language ON songs (title, artist, language); diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 78ff52ce..4849d8a7 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -2,6 +2,7 @@ "anki": { "addNew": "Add new", "backSide": "Back", + "clear": "Clear", "close": "Close", "copyChars": "Copy Characters", "copyPronunciations": "Copy Pronunciations", @@ -19,6 +20,7 @@ "previousPage": "Previous page", "round": "Anki Round", "save": "Save", + "saveCardFailed": "Save failed, probably it already exists", "saveFailed": "Failed to save anki", "startRound": "Start Round", "togglePronunciation": "Toggle Pronunciation", @@ -91,6 +93,7 @@ "close": "Close", "created": "Created", "edit": "Edit", + "filter": "Filter songs", "lang": "Language", "language": "Language", "link": "Link", diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index d2929761..2e91e9b2 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -2,6 +2,7 @@ "anki": { "addNew": "Añadir nueva tarjeta", "backSide": "Parte trasera", + "clear": "Limpiar", "close": "Cerrar", "copyChars": "Copiar caracteres", "copyPronunciations": "Copiar pronunciaciones", @@ -19,6 +20,7 @@ "previousPage": "Página anterior", "round": "Ronda", "save": "Guardar", + "saveCardFailed": "Error al guardar, probablemente ya existe", "saveFailed": "Failed to save anki", "startRound": "Empezar ronda", "togglePronunciation": "Alternar pronunciación", @@ -91,6 +93,7 @@ "close": "Cerrar", "created": "Creado", "edit": "Editar", + "filter": "Buscar canciones", "lang": "Idioma", "language": "Idioma", "link": "Enlace", diff --git a/public/locales/jp/translation.json b/public/locales/jp/translation.json index e392d6b0..078c5e46 100644 --- a/public/locales/jp/translation.json +++ b/public/locales/jp/translation.json @@ -2,6 +2,7 @@ "anki": { "addNew": "新しい Anki カードを追加", "backSide": "背面", + "clear": "クリア", "close": "關閉", "copyChars": "複製文字字元", "copyPronunciations": "複製文字發音", @@ -19,6 +20,7 @@ "previousPage": "前のページ", "round": "ラウンド", "save": "保存", + "saveCardFailed": "保存失敗、おそらくすでに存在しています", "saveFailed": "保存失敗", "startRound": "開始 Anki 回合", "togglePronunciation": "切換顯示發音", @@ -91,6 +93,7 @@ "close": "閉じる", "created": "作成", "edit": "編集", + "filter": "フィルター", "lang": "言語", "language": "言語", "link": "リンク", diff --git a/public/locales/zh_hant/translation.json b/public/locales/zh_hant/translation.json index 8f8e7a50..fea0224c 100644 --- a/public/locales/zh_hant/translation.json +++ b/public/locales/zh_hant/translation.json @@ -2,6 +2,7 @@ "anki": { "addNew": "添新", "backSide": "背面", + "clear": "清除文字", "close": "關閉", "copyChars": "複製文字字元", "copyPronunciations": "複製文字發音", @@ -19,6 +20,7 @@ "previousPage": "上一頁", "round": "回合", "save": "保存", + "saveCardFailed": "保存失敗,可能已經存在", "saveFailed": "保存失敗", "startRound": "開始 Anki 回合", "togglePronunciation": "切換顯示發音", @@ -91,6 +93,7 @@ "close": "關閉", "created": "創建", "edit": "編輯", + "filter": "過濾", "lang": "語言", "language": "語言", "link": "連結", diff --git a/src/backend/db/anki.rs b/src/backend/db/anki.rs index c44a376c..1b985801 100644 --- a/src/backend/db/anki.rs +++ b/src/backend/db/anki.rs @@ -110,15 +110,18 @@ impl Anki { .unwrap_or(None) } - pub fn save(&self) { + pub fn save(&self) -> Result<(), ()> { let connection = &mut establish_connection(); use super::schema::ankis::dsl::*; - diesel::insert_into(ankis) - .values(self) - .execute(connection) - .unwrap(); + let result = diesel::insert_into(ankis).values(self).execute(connection); + + if result.is_err() { + Err(()) + } else { + Ok(()) + } } pub fn delete(&self) { diff --git a/src/backend/db/mod.rs b/src/backend/db/mod.rs index 6f9a9cd2..db2683ca 100644 --- a/src/backend/db/mod.rs +++ b/src/backend/db/mod.rs @@ -1,9 +1,10 @@ -pub use models::{Anki, Text, User}; +pub use models::{Anki, Song, Text, User}; pub use utils::establish_connection; mod anki; mod models; mod schema; +mod song; mod text; mod user; mod utils; diff --git a/src/backend/db/models.rs b/src/backend/db/models.rs index 753b5cc8..a4069929 100644 --- a/src/backend/db/models.rs +++ b/src/backend/db/models.rs @@ -1,7 +1,7 @@ use diesel::{deserialize::Queryable, Insertable}; use serde::Serialize; -use super::schema::{ankis, texts, users}; +use super::schema::{ankis, songs, texts, users}; #[derive(Serialize, Queryable, Insertable, Debug)] pub struct User { @@ -35,3 +35,13 @@ pub struct Anki { pub created_at: chrono::NaiveDateTime, pub updated_at: chrono::NaiveDateTime, } + +#[derive(Serialize, Queryable, Insertable, Debug)] +pub struct Song { + pub id: i32, + pub title: String, + pub artist: String, + pub language: String, + pub video_url: String, + pub lyrics: String, +} diff --git a/src/backend/db/schema.rs b/src/backend/db/schema.rs index ff86a6e0..44400150 100644 --- a/src/backend/db/schema.rs +++ b/src/backend/db/schema.rs @@ -15,6 +15,17 @@ diesel::table! { } } +diesel::table! { + songs (id) { + id -> Integer, + title -> Text, + artist -> Text, + language -> Text, + video_url -> Text, + lyrics -> Text, + } +} + diesel::table! { texts (id) { id -> Text, @@ -41,6 +52,7 @@ diesel::joinable!(texts -> users (user_id)); diesel::allow_tables_to_appear_in_same_query!( ankis, + songs, texts, users, ); diff --git a/src/backend/db/song.rs b/src/backend/db/song.rs new file mode 100644 index 00000000..39365484 --- /dev/null +++ b/src/backend/db/song.rs @@ -0,0 +1,70 @@ +use super::models::Song; +use super::utils::establish_connection; +use diesel::prelude::*; + +impl Song { + fn get_query_filter( + lang: &str, + query: &Option, + ) -> Box< + dyn BoxableExpression< + super::schema::songs::table, + diesel::sqlite::Sqlite, + SqlType = diesel::sql_types::Bool, + >, + > { + use super::schema::songs::dsl::*; + + if query.is_none() { + return Box::new(language.eq(lang.to_owned())); + } + let query = format!("%{}%", query.clone().unwrap()); + + let filter = artist.like(query.clone()).or(title.like(query.clone())); + + Box::new(filter) + } + + pub fn get_all( + lang: String, + items_number: i32, + offset: i32, + query: Option, + ) -> Vec { + let connection = &mut establish_connection(); + + use super::schema::songs::dsl::*; + + songs + .filter(language.eq(&lang)) + .filter(Self::get_query_filter(&lang, &query)) + .limit(items_number.into()) + .offset(offset.into()) + .load::(connection) + .unwrap_or(Vec::new()) + } + + pub fn get_total(lang: String, query: Option) -> i32 { + let connection = &mut establish_connection(); + + use super::schema::songs::dsl::*; + + songs + .filter(Self::get_query_filter(&lang, &query)) + .count() + .get_result(connection) + .unwrap_or(0) as i32 + } + + pub fn get_by_id(song_id: i32) -> Option { + let connection = &mut establish_connection(); + + use super::schema::songs::dsl::*; + + songs + .filter(id.eq(song_id)) + .first(connection) + .optional() + .unwrap_or(None) + } +} diff --git a/src/backend/gql/mod.rs b/src/backend/gql/mod.rs index 7dba34fd..e5c4f7c5 100644 --- a/src/backend/gql/mod.rs +++ b/src/backend/gql/mod.rs @@ -1,7 +1,7 @@ use self::context::GraphQLContext; use crate::backend::{ - db::{Anki, Text}, - gql::models::{AnkiGQL, Me, TextGQL, TranslationRequest}, + db::{Anki, Song, Text}, + gql::models::{AnkiGQL, Me, SongGQL, TextGQL, TranslationRequest}, }; use juniper::{EmptySubscription, FieldResult, RootNode}; use tracing::debug; @@ -87,6 +87,31 @@ impl QueryRoot { Ok(ankis_gql) } + fn songs( + lang: String, + items_num: Option, + offset: Option, + query: Option, + ) -> FieldResult> { + let songs = Song::get_all(lang, items_num.unwrap_or(10), offset.unwrap_or(0), query); + let songs_gql = songs.iter().map(SongGQL::from_db).collect::>(); + + Ok(songs_gql) + } + + fn song(id: i32) -> FieldResult> { + let song = Song::get_by_id(id); + let song_parsed = song.map(|s| SongGQL::from_db(&s)); + + Ok(song_parsed) + } + + fn songs_total(lang: String, query: Option) -> FieldResult { + let songs_total = Song::get_total(lang, query); + + Ok(songs_total) + } + async fn translation_request( ctx: &GraphQLContext, content: String, @@ -124,7 +149,11 @@ impl MutationRoot { if anki.front.is_empty() { anki.delete(); } else { - anki.save(); + let result = anki.save(); + + if result.is_err() { + return Err("Could not save the record".into()); + } } Ok(AnkiGQL::from_db(&anki)) diff --git a/src/backend/gql/models.rs b/src/backend/gql/models.rs index 54484016..a18e8e7c 100644 --- a/src/backend/gql/models.rs +++ b/src/backend/gql/models.rs @@ -1,5 +1,5 @@ use crate::backend::{ - db::{Anki, Text}, + db::{Anki, Song, Text}, translation::translate_text, }; use juniper::GraphQLObject; @@ -42,6 +42,16 @@ pub struct TranslationRequest { current_language: String, } +#[derive(GraphQLObject)] +pub struct SongGQL { + artist: String, + id: i32, + language: String, + lyrics: String, + title: String, + video_url: String, +} + impl Me { pub fn new(id: &str, email: &str, can_use_ai: bool) -> Self { Self { @@ -77,6 +87,19 @@ impl AnkiGQL { } } +impl SongGQL { + pub fn from_db(song: &Song) -> Self { + Self { + artist: song.artist.to_string(), + id: song.id, + language: song.language.to_string(), + lyrics: song.lyrics.to_string(), + title: song.title.to_string(), + video_url: song.video_url.to_string(), + } + } +} + impl TranslationRequest { pub fn new(content: &str, current_language: &str) -> Self { Self { diff --git a/src/backend/mod.rs b/src/backend/mod.rs index bfe0bcb1..1d3d0025 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -9,7 +9,7 @@ use chrono::{Duration, Utc}; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; use jsonwebtoken::{encode, EncodingKey, Header}; use juniper::http::{graphiql::graphiql_source, GraphQLRequest}; -use tracing::{debug, warn}; +use tracing::{debug, info, warn}; use uuid::Uuid; use crate::backend::{ @@ -179,6 +179,7 @@ pub async fn start_backend() -> std::io::Result<()> { let connection = &mut db::establish_connection(); connection.run_pending_migrations(MIGRATIONS).unwrap(); + info!("Migrations completed"); warn!("Starting the Writing Trainer HTTP server on port http://{address}:{port}"); diff --git a/src/react-ui/containers/AnkisSection/AnkisSection.tsx b/src/react-ui/containers/AnkisSection/AnkisSection.tsx index a5a41863..f05eef19 100644 --- a/src/react-ui/containers/AnkisSection/AnkisSection.tsx +++ b/src/react-ui/containers/AnkisSection/AnkisSection.tsx @@ -41,9 +41,9 @@ const AnkiRound = ({ ankisRound, setAnkisRound }: AnkisRoundProps) => { const currentAnki = ankisRound?.[currentIndex] return ( -
+
{t('anki.round')}
-
+
- - +
+ + + +
{ 應該隱藏發音={!showPronunciation} />
-
+
+ diff --git a/src/react-ui/containers/Panel/Panel.tsx b/src/react-ui/containers/Panel/Panel.tsx index d0c1d6ad..c79f2990 100644 --- a/src/react-ui/containers/Panel/Panel.tsx +++ b/src/react-ui/containers/Panel/Panel.tsx @@ -445,6 +445,7 @@ const Panel = ({ return ( { clearValues() diff --git a/src/react-ui/containers/RecordsSection/RecordsSection.tsx b/src/react-ui/containers/RecordsSection/RecordsSection.tsx index c785ea5e..07a0270a 100644 --- a/src/react-ui/containers/RecordsSection/RecordsSection.tsx +++ b/src/react-ui/containers/RecordsSection/RecordsSection.tsx @@ -1,11 +1,9 @@ import { LanguageDefinition, Record } from '#/core' import { TextGql } from '#/react-ui/graphql/graphql' -import { backendClient } from '#/react-ui/lib/backendClient' -import { useCallback, useEffect, useState } from 'react' +import { backendClient, SongItem } from '#/react-ui/lib/backendClient' +import { useCallback, useEffect, useRef, useState } from 'react' import { toast } from 'react-toastify' -import { songs as cantoneseSongs } from '../../languages/cantonese/songs' -import { songs as mandarinSongs } from '../../languages/mandarin/songs' import { T_Services } from '../../typings/mainTypes' import { useMainContext } from '../main-context' @@ -49,6 +47,7 @@ const getInitialRecord = ({ type IProps = { initScreen: RecordsScreen + language: string onRecordLoad: (r: Record) => void onRecordsClose: () => void onSongLoad: (s: string[]) => void @@ -61,6 +60,7 @@ type IProps = { // @TODO: Remove local/remote records and use a offline syncing librar const RecordsSection = ({ initScreen, + language, onRecordLoad, onRecordsClose, onSongLoad, @@ -74,9 +74,18 @@ const RecordsSection = ({ null, ) const [records, setRecords] = useState([]) + const [songs, setSongs] = useState<{ list: SongItem[]; total: number }>({ + list: [], + total: 0, + }) const [isLoading, setIsLoading] = useState(false) + const [songsFilter, setSongsFilter] = useState('') + + const songsDebounceRef = useRef(null) const mainContext = useMainContext() + const { isBackendActive } = mainContext.state + const { storage } = services const retrieveRecords = useCallback(async () => { @@ -121,10 +130,40 @@ const RecordsSection = ({ setIsLoading(false) }, [pronunciation, storage]) + const retrieveSongs = useCallback(async () => { + if (!isBackendActive) return + + if (songsDebounceRef.current) { + clearTimeout(songsDebounceRef.current) + } + + songsDebounceRef.current = window.setTimeout(async () => { + const timeoutVal = songsDebounceRef.current + setIsLoading(true) + + const newSongs = await backendClient + .getSongs(language, songsFilter, 100, 0) + .catch(() => ({ list: [], total: 0 })) + + if (timeoutVal !== songsDebounceRef.current) { + return + } + + setSongs(newSongs) + + setIsLoading(false) + songsDebounceRef.current = null + }, 1000) + }, [language, songsFilter, isBackendActive]) + useEffect(() => { retrieveRecords().catch(() => {}) }, [retrieveRecords]) + useEffect(() => { + retrieveSongs().catch(() => {}) + }, [retrieveSongs]) + const saveRecord = (newRecord: Record) => { let newRecords = [...records] const existingRecordIndex = newRecords.findIndex(r => r.id === newRecord.id) @@ -275,7 +314,9 @@ const RecordsSection = ({ onRecordRemove={handleRecordRemove} onSongLoad={onSongLoad} records={records} - songs={cantoneseSongs.concat(mandarinSongs)} + setSongsFilter={setSongsFilter} + songs={songs} + songsFilter={songsFilter} /> ) diff --git a/src/react-ui/containers/RecordsSection/screens/RecordsList.tsx b/src/react-ui/containers/RecordsSection/screens/RecordsList.tsx index 18c2f952..6ad99651 100644 --- a/src/react-ui/containers/RecordsSection/screens/RecordsList.tsx +++ b/src/react-ui/containers/RecordsSection/screens/RecordsList.tsx @@ -1,4 +1,5 @@ import { Record } from '#/core' +import { backendClient, SongItem } from '#/react-ui/lib/backendClient' import { TOOLTIP_ID } from '#/utils/tooltip' import { Fragment, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -52,14 +53,6 @@ const formatRecordDate = (d: number): string => { return `[${dateStr}]` } -type 歌曲類型 = { - artist: string - lang: string - load: () => Promise<{ lyrics: string[] }> - name: string - video: string -} - type 條目清單屬性 = { disabled: boolean onRecordEdit: (r: Record) => void @@ -67,7 +60,9 @@ type 條目清單屬性 = { onRecordRemove: (r: Record) => void onSongLoad: (s: string[]) => void records: Record[] - songs: 歌曲類型[] + setSongsFilter: (s: string) => void + songs: { list: SongItem[]; total: number } + songsFilter: string } const RecordsList = ({ @@ -77,7 +72,9 @@ const RecordsList = ({ onRecordRemove, onSongLoad, records, + setSongsFilter, songs, + songsFilter, }: 條目清單屬性) => { const { t } = useTranslation() const [filterText, setFilterText] = useState('') @@ -105,88 +102,101 @@ const RecordsList = ({ />
)} -
- {filteredList.map(filteredItem => { - const { createdOn, id, lastLoadedOn, name } = filteredItem + {!!filteredList.length && ( +
+ {filteredList.map(filteredItem => { + const { createdOn, id, lastLoadedOn, name } = filteredItem - return ( -
- - - - - {filteredItem.link && ( - - {t('record.link')} - - )} -
- - - + return ( +
+ + + + + {filteredItem.link && ( + + {t('record.link')} + + )} +
+ + + +
-
- ) - })} - {songs.map(歌曲 => { - const { artist, lang, load, name, video } = 歌曲 - const 影片連結網址 = video.startsWith('https://') - ? video - : `https://www.youtube.com/watch?v=${video}` + ) + })} +
+ )} + +
+ { + setSongsFilter(e.target.value) + }} + onEnterPress={() => {}} + placeholder={t('record.filter', 'Filter songs')} + value={songsFilter} + /> + {songs.list.map(song => { + const { artist, id: songId, title, videoUrl } = song + const 影片連結網址 = videoUrl.startsWith('https://') + ? videoUrl + : `https://www.youtube.com/watch?v=${videoUrl}` return (
- + - - {video && ( + {videoUrl && ( { - load().then(({ lyrics }) => { - onSongLoad(lyrics) + backendClient.getSongLyrics(song.id).then(({ lyrics }) => { + onSongLoad((lyrics ?? '').split('\n')) }) }} > diff --git a/src/react-ui/graphql/graphql.ts b/src/react-ui/graphql/graphql.ts index 837d0a58..eae138fe 100644 --- a/src/react-ui/graphql/graphql.ts +++ b/src/react-ui/graphql/graphql.ts @@ -78,6 +78,9 @@ export type QueryRoot = { ankisRound: Array ankisTotal: Scalars['Int']['output'] me: Me + song?: Maybe + songs: Array + songsTotal: Scalars['Int']['output'] texts: Array translationRequest: Scalars['String']['output'] } @@ -96,11 +99,37 @@ export type QueryRootAnkisTotalArgs = { query?: InputMaybe } +export type QueryRootSongArgs = { + id: Scalars['Int']['input'] +} + +export type QueryRootSongsArgs = { + itemsNum?: InputMaybe + lang: Scalars['String']['input'] + offset?: InputMaybe + query?: InputMaybe +} + +export type QueryRootSongsTotalArgs = { + lang: Scalars['String']['input'] + query?: InputMaybe +} + export type QueryRootTranslationRequestArgs = { content: Scalars['String']['input'] currentLanguage: Scalars['String']['input'] } +export type SongGql = { + __typename?: 'SongGQL' + artist: Scalars['String']['output'] + id: Scalars['Int']['output'] + language: Scalars['String']['output'] + lyrics: Scalars['String']['output'] + title: Scalars['String']['output'] + videoUrl: Scalars['String']['output'] +} + export type TextGql = { __typename?: 'TextGQL' body: Scalars['String']['output'] diff --git a/src/react-ui/languages/cantonese/songs/index.ts b/src/react-ui/languages/cantonese/songs/index.ts deleted file mode 100644 index 21a15152..00000000 --- a/src/react-ui/languages/cantonese/songs/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -const handleSongTreble = - (artist: string) => - ([load, name, video]: [ - () => Promise<{ lyrics: string[] }>, - string, - string, - ]) => ({ - artist, - lang: 'cantonese', - load, - name, - video, - }) - -const kayTseSongs = [ - [() => import('./kay-tse-saan-lam-dou'), '山林道', 'W4q4XHhDM-c'], - [() => import('./kay-tse-nei-mun-dik-hang-fuk'), '你們的幸福', 'oYgMRIIVX3w'], - [() => import('./kay-tse-nin-dou-zi-go'), '年度之歌', 'XAobAFsWTy8'], - [() => import('./kay-tse-zoi-ngo-zau'), '載我走', 'wboL_3_StIA'], - [() => import('./kay-tse-lei-bat-hoi'), '離不開', 'pNDI9oBG7po'], - [() => import('./kay-tse-faat-jyu-cing'), '法與情', 'qjvAG2Y2LaE'], -].map(handleSongTreble('Kay Tse')) - -const myLittleAirportSongs = [ - [ - () => import('./my-little-airport-naa-zan-si-bat-zi-dou'), - '那陣時不知道', - '5xi49OreS3k', - ], - [ - () => - import('./my-little-airport-nin-hing-dik-caa-cang-ting-lou-baan-loeng'), - '年輕的茶餐廳老闆娘', - 'dh4eydYk6EA', - ], - [ - () => import('./my-little-airport-heoi-seon-wo-maai-dip'), - '去信和賣碟', - 'gQHmVvwagZw', - ], - [ - () => import('./my-little-airport-ci-go-mou-gaai'), - '詩歌舞街', - 'J2DxGXp8KYY', - ], - [ - () => import('./my-little-airport-zoi-saat-jat-go-jan'), - '再殺一個人', - 'R53S8JJw5dY', - ], - [ - () => import('./my-little-airport_jat-go-ngaan-san'), - '一個眼神', - 'Zv7Ty9NuLKM', - ], -].map(handleSongTreble('My Little Airport')) - -const janiceVidalSongs = [ - [ - () => import('./janice-vidal-jat-gaak-gaak'), - '一格格 Frames', - 'N1jdWcmEv0Q', - ], -].map(handleSongTreble('Janice Vidal')) - -export const songs = kayTseSongs - .concat(myLittleAirportSongs) - .concat(janiceVidalSongs) - .concat( - [ - [() => import('./stephy-tang_din-dang-daam'), '電燈膽', 'IDywqSyQ3Mc'], - ].map(handleSongTreble('Stephy Tang')), - ) diff --git a/src/react-ui/languages/cantonese/songs/janice-vidal-jat-gaak-gaak.ts b/src/react-ui/languages/cantonese/songs/janice-vidal-jat-gaak-gaak.ts deleted file mode 100644 index 455e0652..00000000 --- a/src/react-ui/languages/cantonese/songs/janice-vidal-jat-gaak-gaak.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const lyrics = [ - '每次上機 都幻想, 誰帶我遠飛 找夢鄉, 談情令我心癢癢, 期待著 甚麼人 會遇上', - - '旅客揭揭袋裝小說便發夢, 教堂有鐘聲 詩班要合唱, 到處看看漫天花瓣 難以想像, 期待著 甚麼人 會遇上', - - '纏住吻住春風吹住我嗎, 纏住吻住鬱金香是你嗎, 纏住吻住詩畫歌頌愛嗎, 拍 逐幅逐幅戀愛定格, 纏住吻住cream cheese點綴我嗎, 纏住吻住古雕刻似你嗎, 纏住愛慢身邊一拍 輕輕一拍, 再捕捉捕捉戀愛定格', - - '遠處有個歷史堡壘在看霧, 快餐店燈牌逐漸明亮, 儲滿已到站的車票 在我身上, 期待著 甚麼人 會遇上', - - '纏住吻住春風吹住我嗎, 纏住吻住鬱金香是你嗎, 纏住吻住詩畫歌頌愛嗎, 拍 逐幅逐幅戀愛定格, 纏住吻住cream cheese點綴我嗎, 纏住吻住古雕刻似你嗎, 纏住愛慢身邊一拍 輕輕一拍, 再捕捉捕捉戀愛定格', - - '前事全放下 遊歷尋覓我的家 願意嗎', - - '纏住吻住春風吹住我嗎, 纏住吻住鬱金香是你嗎, 一呼一吸知道愛 一點一拍, 逐幅逐幅戀愛定格, 纏住吻住cream cheese點綴我嗎, 纏住吻住古雕刻似你嗎, 纏住愛慢身邊一拍 輕輕一拍, 再捕捉捕捉戀愛定格', - - '然後我登機 就此完了, 懷念著 甚麼人 我遇上, 懷念著 這個人 我遇上', -] diff --git a/src/react-ui/languages/cantonese/songs/kay-tse-faat-jyu-cing.ts b/src/react-ui/languages/cantonese/songs/kay-tse-faat-jyu-cing.ts deleted file mode 100644 index 2d83566f..00000000 --- a/src/react-ui/languages/cantonese/songs/kay-tse-faat-jyu-cing.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const lyrics = [ - '民事或刑事, 盤問犯人為了公義, 斜視或無視, 來讓敵人沒計可施, 憑著這德性, 成敗得失中 拼命, 從沒有合理懷疑, 要讓世代重獲道理及良 知', - - '庭內沒承讓, 權力或權利也一樣, 庭外立神像, 蒙著目仍復見光亮, 無望可得勝, 頭上天秤逐漸 往下沉, 沉下了又再上揚, 世事變幻徐疾似樂 章', - - '制定每樣規則是法, 讓世間遵守所有規矩成律, 但有些隱蔽的罅隙, 窺見的血跡, 就算用旋律仍沒法闡述', - - '自信可過渡每日相擁是愛, 處理好任何事情沒有傷害, 但有些相處的罅隙, 傷痛的記憶, 任韻律再轉變沒法可 離開', - - '唯獨是情字, 謀事在人但欠本事, 無奈是情事, 成事在誰沒理可依, 無罪終釋放, 然後不再喚起 那誓盟, 平步已踏上白雲, 再沒氣力來面對緣 份', - - '制定每樣規則是法, 讓世間遵守所有規矩成律, 但有些隱蔽的罅隙, 窺見的血跡, 就算用旋律仍沒法闡述', - - '自信可過渡每日相擁是愛, 處理好任何事情沒有傷害, 但有些相處的罅隙, 傷痛的記憶, 任韻律再轉變沒法可 避開', - - '銀白色假髮, 或可掩飾默契早 不存在, 情共法合奏未來, 控辯對峙 矛或盾誰又願放開', - - '制定每樣規則是法, 讓世間遵守所有絕對規律, 但有些隱蔽的罅隙, 真相不破壁, 沒法律裁定如像變魔術', - - '過渡每日相擁是愛, 處理好任何事情沒有傷害, 但有些相處的罅隙, 仿似不進逼', -] diff --git a/src/react-ui/languages/cantonese/songs/kay-tse-lei-bat-hoi.ts b/src/react-ui/languages/cantonese/songs/kay-tse-lei-bat-hoi.ts deleted file mode 100644 index 77865fd0..00000000 --- a/src/react-ui/languages/cantonese/songs/kay-tse-lei-bat-hoi.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const lyrics = [ - '我的思緒 離不開, 說過的一切 已不再, 不再是我們 所愛, 剎那說穿了 全沒意外', - - '裂口中倉卒成長, 成長必經的惆悵, 大概是我們 紛擾中 最多的進帳', - - '季節交替 未更改, 但冷暖不似 往昔記載, 霜雪暴雨入江海, 灼痛最傷處️ 流入眼內', - - '盤中蛙不會自知, 墨守水裏不明智, 別要學會嚐 這一口 刺骨的暖意', - - '回頭沒有 身後人, 誰在為遠山 記恨, 輾轉 歲月星移, 千秋 散亂於此, 紅眼睛碰滿天星的剌, 悲傷不已', - - '太多次假裝 不傷心, 寧願帶笑共你 慶祝春分, 熱極了 小島這黃昏, 有多不配襯', - - '從來沒有 身後人, 無緣分都 不記恨, 天陰缺月 有時, 煙火散落 不止, 從倒帶看的 春風得意, 再聚何時, 我所愛的 我不改, 甜苦悲歡用我 承載', - - '汗滴掛 春天的雲彩, 涙落遍 天一方這星海, 也裝滿似春風暖的愛, 願花開 心無罣礙', -] diff --git a/src/react-ui/languages/cantonese/songs/kay-tse-nei-mun-dik-hang-fuk.ts b/src/react-ui/languages/cantonese/songs/kay-tse-nei-mun-dik-hang-fuk.ts deleted file mode 100644 index 44cfdd66..00000000 --- a/src/react-ui/languages/cantonese/songs/kay-tse-nei-mun-dik-hang-fuk.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const lyrics = [ - '等不到想等愛情侶 便已擁抱到熟睡, 經不起錯對 但已找到樂趣', - '抽不出一刻空虛 已經得到你所需, 成了好一對 忙著去策劃更好的以後 想不起想鬥嘴', - - '人間多少瘡疤見報 若太深奧懶得知道, 斑點狗太瘦 是與他僅有的煩惱', - '醒於不安的清早 你竟然從沒遇到, 你沒有心思控訴 從未曾怕累怕趕只畏懼枯燥', - - '你失去耐性失落 飲飽吃醉是容易極的快樂, 牽手看偶像連續劇哭哭笑笑', - '輕輕鬆鬆 將恩怨情慾變娛樂, 愛思索便會福薄 砂吹進眼內從來未覺便不必發覺', - '吹熄了那火花 煙火會圍住了這恩愛王國', - - '你 還會有什麼感想需要痛哭 還欠缺什麼東西不夠滿足', - '填密每天的一秒 還自覺有幸繁忙是一種祝福, 真有福 不能停下什麼都不敢結束', - '從不曾沉悶大家已能找到最好歸宿', - - '如果有妄想堅決不懂 美滿得不接受心痛', - - '你失去耐性失落 飲飽吃醉是容易極的快樂, 牽手看偶像連續劇哭哭笑笑', - '輕輕鬆鬆 將恩怨情慾變娛樂, 愛思索便會福薄 砂吹進眼內從來未覺便不必發覺', - '吹熄了那火花 煙火會圍住了這恩愛硬殼', - - '你 還會有什麼感想需要痛哭 還欠缺什麼東西不夠滿足, 填密每天的一秒 還自覺有幸繁忙是一種祝福', - '真有福 不能停下什麼都不敢結束, 從不曾沉悶大家已能找到最好歸宿', - '如果有妄想堅決不懂 美滿得不接受心痛', - - '你 還會有什麼感想需要痛哭 還欠缺什麼東西不夠滿足, 填密這一生一秒 還自覺有幸繁忙是一種祝福', - '抵抗孤獨 不能停下什麼都不敢結束, 從不曾沉悶日子變成節目延續節目', - '漸漸馴服得不記得哭 美滿得不接受心痛, 你也許比我易滿足 你也許比我幸福 幸福地麻木', -] diff --git a/src/react-ui/languages/cantonese/songs/kay-tse-nin-dou-zi-go.ts b/src/react-ui/languages/cantonese/songs/kay-tse-nin-dou-zi-go.ts deleted file mode 100644 index e2b13a33..00000000 --- a/src/react-ui/languages/cantonese/songs/kay-tse-nin-dou-zi-go.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const lyrics = [ - '曾經攀上的天梯 曾經擁抱的身體, 曾經在乎一切 被突然摧毀 剎那比沙更細', - '良夜美景 沒原因出了軌, 來讓我知 一切皆可放低', - '還是百載未逢的美麗, 得到過 又猝逝 也有一種智慧', - - '全年度有幾多首歌 給天天的播, 給你最愉快的消磨', - '流行是一首窩心的歌, 突然間說過就過', - - '誰曾是你這一首歌 你記不清楚, 我看著你離座', - '真高興給你愛護過, 根本你不欠我什麼', - - '曾經擁有的春季 曾經走過的谷底, 人生是場興替 忽高也忽低 不輸氣勢', - - '全年度有幾多首歌 給天天的播, 給你最愉快的消磨', - '流行是一首窩心的歌, 突然間說過就過', - - '誰曾是你這一首歌 你記不清楚, 我看著你離座', - '真高興給你愛護過, 根本你不欠我什麼', - - '誰曾是你這一首歌 你記不清楚, 我看著你離座', - '很高興因你燦爛過, 高峰過總會有下坡', - - '回憶裝滿的抽屜 時光機裡的光輝, 人生艷如花卉 但限時美麗 一覽始終無遺', - '回望昨天 劇場深不見底, 還是有幾幕曾好好發揮', - '還願我懂下台的美麗, 鞠躬了 就退位 起碼得到敬禮', - - '誰又妄想一曲一世 讓人忠心到底', -] diff --git a/src/react-ui/languages/cantonese/songs/kay-tse-saan-lam-dou.ts b/src/react-ui/languages/cantonese/songs/kay-tse-saan-lam-dou.ts deleted file mode 100644 index 1271b9c4..00000000 --- a/src/react-ui/languages/cantonese/songs/kay-tse-saan-lam-dou.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const lyrics = [ - '昨日是 小小鬥志, 祈求突破悶局 闖一次, 劈下樹 開山兩次, 由無路 變吋吋 慢慢移', - - '要實現 當天壯語, 沿途活得相當 有意義, 今天也算 爲世間貢獻, 卻怕轉折處 欠勵志', - - '妄想 這裡有天 會由樹 變成路, 一醒覺 經已殺出 這條路, 叢林萬里 別攔著我, 舊時熱情又急躁 不看地圖', - - '我只盼 這裡有天 變回樹 撤回路, 疏忽了 趕快去補 趁還未老, 遺落美好枝~~葉 換到好前途 皆負數, 時候不早了 但總算 知道', - - '我活著 只因我愛 然而沒法事事 都裝載, 昨日共 青春競賽 狼忙像 錯過了 沒後來', - - '吃玩睡 身邊至愛 流程外的統統 當障礙, 花花世界 用半生灌溉 我卻荒廢了 某樹海', - - '當初說 這裡有天 會由樹 變成路, 一醒覺 經已殺出 這條路, 叢林萬里 別攔著我, 舊時熱情又急躁 不看地圖', - - '我只盼 這裡有天 變回樹 撤回路, 疏忽了 趕快去補 趁還未老, 遺落美好枝~~葉 換到好前途 皆負數, 無謂到 所有樹枯了 才環抱', - - '那蔥綠年華 及態度, 若未忘未棄 就拾回原稿, 回去 原地照計劃來做', - - '初出發那天 盼由樹 變成路, 一醒覺 經已殺出 這條路, 人情物理 直行直過, 若然後遺未清掃 可否盡掃', - - '太意想不到 一切困境 會平定 變成路, 犧牲最寶貴那些 也難料到, 然後賺了獎~~項 又想走回頭 改定數, 時候不早了 別等到 情懷老', - - '問我初衷 鏗鏘的答 不吞吐', -] diff --git a/src/react-ui/languages/cantonese/songs/kay-tse-zoi-ngo-zau.ts b/src/react-ui/languages/cantonese/songs/kay-tse-zoi-ngo-zau.ts deleted file mode 100644 index e8275dbc..00000000 --- a/src/react-ui/languages/cantonese/songs/kay-tse-zoi-ngo-zau.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const lyrics = [ - '已發動了 連汽笛也呼叫, 時鐘窗框中退出了', - '慢慢往相反方向變淡變小, 上路了 乘這班車遠走了, 囤積工作懶得照料 逃去夭夭', - - '望向望向十里外湖水, 感到睏便去睡 彷彿走進太虛', - '覺餓了 踏足餐卡裡, 和窗邊歌德式的古堡茶敘', - - '還好有你在 讓我喘息悠閒下來, 就算身處洪流中 安然能無礙', - '你是這段鐵路歷程 無比慷慨, 載我出走一趟 創傷暫時拋開', - - '全因有你在 令我不須長留月台, 就算三軍臨城中 車門仍敞開', - '一起走稍稍作歇息 鄉郊中望海, 遺下兵荒都市 繼續競賽', - - '太過忙了 人要追每分秒, 無非想找一剎心跳', - '但是太多將輕重錯誤對焦, 抱住了 環抱得到最閃耀, 才知震撼卻得不了 心都不跳', - - '是你令我沒有拼命追, 知到最重要是 簡單得似淡水', - '載著我渡窩心之旅, 毋須分分鐘交出驚天壯舉', - - '還好有你在 讓我喘息悠閒下來, 就算身處洪流中 安然能無礙', - '你是這段鐵路歷程 無比慷慨, 載我出走一趟 創傷暫時拋開', - - '全因有你在 令我不須長留月台, 就算三軍臨城中 車門仍敞開', - '一起走稍稍作歇息 鄉郊中望海, 遺下兵荒都市 繼續競賽', - - '沿路風光的美 震撼腦袋', -] diff --git a/src/react-ui/languages/cantonese/songs/my-little-airport-ci-go-mou-gaai.ts b/src/react-ui/languages/cantonese/songs/my-little-airport-ci-go-mou-gaai.ts deleted file mode 100644 index 65a27e05..00000000 --- a/src/react-ui/languages/cantonese/songs/my-little-airport-ci-go-mou-gaai.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const lyrics = [ - '那晚看Russian Red大角咀表演, 你與我竟會再遇見, 剛好我也到bar枱買酒才碰面, 完場與你去深水埗那邊, 剛過身的作家 你介紹那年, 寫過只有散步我們才真正聊天', - - '再次聽你說故事, 聽你說剛生的嬰兒, 詩歌舞街 地上有著光點閃閃, 聽你說外地歷險, 我卻像當天的少年, 但已是 能接受你所有改變', - - '你說你最近愛聽古典, 你說你父母開始婚變, 邊聽邊講我忘了怎去99咖啡店, 說到我倆澳門過生日那年, 你為我訂了山上的酒店, 餐廳老闆對你彈唱結他一整天', - - '再次聽你說故事, 聽你說剛生的嬰兒, 走進萬發 坐在暗光卡位窗邊, 聽你說外地歷險, 我卻像當天的少年, 但已是 能控制我所有淚腺', -] diff --git a/src/react-ui/languages/cantonese/songs/my-little-airport-heoi-seon-wo-maai-dip.ts b/src/react-ui/languages/cantonese/songs/my-little-airport-heoi-seon-wo-maai-dip.ts deleted file mode 100644 index 83260ca8..00000000 --- a/src/react-ui/languages/cantonese/songs/my-little-airport-heoi-seon-wo-maai-dip.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const lyrics = [ - '到你再過多兩年, 差不多三十歲的某天, 你會去最後一次二手唱片店, 賣去過往十數年, 買落的幾百張唱片, 這個時候你要賣得有技巧一點', - - '技巧是要知道有兩種唱片店, 一種是每張碟報不同價錢, 一種是一個價錢報一堆唱片, 你要以第一種店為先, 它不會收下所有唱片, 餘下的就再賣去第二種店', - - '這次序當中的重點, 不只可整體賣高一倍價錢, 還可讓你盡量忘掉眼前, 事關一下子失去所有唱片, 會有把夢燒光的感覺出現, 忙著分析價錢有助讓這感覺不見', - - '鳴謝旺角信和「雷射唱片交易所」及「古今」', -] diff --git a/src/react-ui/languages/cantonese/songs/my-little-airport-naa-zan-si-bat-zi-dou.ts b/src/react-ui/languages/cantonese/songs/my-little-airport-naa-zan-si-bat-zi-dou.ts deleted file mode 100644 index 9beaaf7b..00000000 --- a/src/react-ui/languages/cantonese/songs/my-little-airport-naa-zan-si-bat-zi-dou.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const lyrics = [ - '如像九十年代初, 照片裡海灘的印象, 那種下午沒有重量, 帶點迷惘奔向夕陽', - - '那年獨自去南洋, 一杯白咖啡的早上, 四周是可冒險方向, 尤加利樹葉的香', - - '那陣時不知道, 置身的日子都發亮, 眼光裡藏著的囂張, 往後已不再同樣', - - '圖書館木製的窗, 訓導假裝嚴肅的模樣, 還未有傾訴對象, 仍未覺得悲涼', - - '那陣時不知道, 每天在風光的戰場, 騎著馬去到邊疆, 所到之地全是賜賞, Woo 寂靜 但不漫長 (x4)', -] diff --git a/src/react-ui/languages/cantonese/songs/my-little-airport-nin-hing-dik-caa-cang-ting-lou-baan-loeng.ts b/src/react-ui/languages/cantonese/songs/my-little-airport-nin-hing-dik-caa-cang-ting-lou-baan-loeng.ts deleted file mode 100644 index 15b546c3..00000000 --- a/src/react-ui/languages/cantonese/songs/my-little-airport-nin-hing-dik-caa-cang-ting-lou-baan-loeng.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const lyrics = [ - '這是我最後一次, 來吃午飯吃到四點, 以後或不會再見, 到底我也是高傲的成年', - - '已考慮不知幾多遍, 才決定給妳這信件, 寫了電話等你致電, 明知你看了只會笑一百遍', - - '這些年裡面, 你都知我很少到別的店, 但若你最終給我來電, 你可以知道這感覺會點', - - '像一支小蠟燭的光點, 讓漆黑的房佈滿光線, 像打開考試試卷發現, 突然所有答案都看得見', - - '對上一次冬至, 來吃晚飯吃到八點, 但你卻沒有出現, 出現我也不敢看你正面', - - '已幻想不知幾多遍, 旅行有你在我身邊, 多次見你髮型轉變, 又是為吸引那個他的視線', - - '這些年裡面, 你都知我很少到別的店, 但若你最終給我來電, 你可以知道這感覺會點', - - '像一支小蠟燭的光點, 讓漆黑的房佈滿光線, 像打開考試試卷發現, 突然所有答案都看得見', - - '這些年裡面, 我也不知道我可以去邊, 但若你最終給我來電, 我可給妳一整個的春天', -] diff --git a/src/react-ui/languages/cantonese/songs/my-little-airport-zoi-saat-jat-go-jan.ts b/src/react-ui/languages/cantonese/songs/my-little-airport-zoi-saat-jat-go-jan.ts deleted file mode 100644 index e61fee40..00000000 --- a/src/react-ui/languages/cantonese/songs/my-little-airport-zoi-saat-jat-go-jan.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const lyrics = [ - '有人話世間所有感情最終都會走向末路, 最好可以死喺三十七歲之前或者更早, 有理想嘅文青到某個階段開始變得深奧, 簡單嘅字句要有學術字眼先表達到', - - '每個作家都覺得自己嘅作品最純粹最樸素, 市面賣得好嘅一般都有危害社會嘅元素, 無人會諗自己嘅才能係咪需要進步, 但總會覺得自己嘅地位有待提高', - - '尤其是你 為什麼可以這樣做, 你都知道 我曾對你很好, 但我從不會表現憤怒, 某天先知道碰到你 眼也不想一掃', - - '我想生活喺一千年前嗰陣時未有電腦, 唯一唔捨得嘅係冬天嘅時候無熱水爐, 近年我嘅生活開始無咁刺激無咁虛無, 以前深夜我會打俾人講我殺咗人一時糊塗', - - '但第日完全唔記得做乜我會咁做, 我懷念嗰陣時每日每夜都好emo, 你係我嘅青春期永遠唔會再接觸到, 兩個人就咁永遠疏離都無話好定唔好', - - '尤其是你 為什麼可以這樣做, 你都知道 我曾對你很好, 但我從不會表現憤怒, 某天先知道碰到你 眼也不想一掃', -] diff --git a/src/react-ui/languages/cantonese/songs/my-little-airport_jat-go-ngaan-san.ts b/src/react-ui/languages/cantonese/songs/my-little-airport_jat-go-ngaan-san.ts deleted file mode 100644 index 2881b853..00000000 --- a/src/react-ui/languages/cantonese/songs/my-little-airport_jat-go-ngaan-san.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const lyrics = [ - '冬天來到 十時也太早, 賴在床上 聽多首歌便上路, 如不想跟他一起, 是否應停止對他好?', - - '幾多筒菲林才會知道, 兩個人原是不同channel, 離開他等於同時, 離開了歡笑與煩惱', - - '一邊想去無人的島, 根本沒有這張地圖, 一幅掛在牆上的肖像, 古人也在時間的籠牢', - - '太多刺身還是油彩毒素, 那夜病床上 突然領悟到, 平時忘記了, 其實我會一個終老', - - '眼睛閉上見到彩圖, 十五世紀的那幅《人間樂土》, 面前的牙膏和嘔吐物, 是否可當做顏料用途?', - - '關機的我如常在城堡, 只因那過高的敏感程度, 彷彿我就是你說的那樣, 不能為別人內心打掃', - - '跟看更點頭 以為他見到, 他卻向探訪的朋友投訴, 說我從不曾對任何人, 打招呼問好', - - '你說教我如何對望不會失措, 你說你平時只會收費才做, 我問:「幾錢一個眼神?」, 你沒回答這個 永恆的問號', -] diff --git a/src/react-ui/languages/cantonese/songs/stephy-tang_din-dang-daam.ts b/src/react-ui/languages/cantonese/songs/stephy-tang_din-dang-daam.ts deleted file mode 100644 index 176535b6..00000000 --- a/src/react-ui/languages/cantonese/songs/stephy-tang_din-dang-daam.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const lyrics = [ - '假使不能公開妒忌 學習大方接受, 同行時要殿後 誰冷落舊朋友', - '節日約我三位一體的慶祝, 沿途明亮燈飾閃映著沈重, 言談越熾熱 內在更冰凍', - '誰當初無心將兩方綴合, 然後留低 只得這寂寞人, 仍是你們密友 呆望你們熱吻, 應該傷感還是快感', - '能回避嘛 我怕了 當那電燈膽, 黏著你們 來來回 委曲中受難, 一個我被撇低 卻又很不慣, 要走的一剎又折返', - '能承認嘛 我故意當那電燈膽, 他日你們完場時 入替也不難', - '善良人埋藏著最壞的心眼, 妄想一天你們會散 會選我嗎', - - '對換了你身份可應該滿足, 情人還是知己都擁入懷抱, 同情或眼淚 讓別個得到', - '留低的原因 一世的秘密, 其實明知只得我是外人, 仍是你們密友 呆望你們熱吻, 應該開心還是痛心', - '能回避嘛 我怕了 當那電燈膽, 黏著你們 來來回 委曲中受難, 一個我被撇低 卻又很不慣, 要走的一剎又折返', - - '能承認嘛 我故意當那電燈膽, 他日你們完場時 入替也不難', - '善良人埋藏著最壞的心眼, 妄想一天你們會散 會選我嗎', -] diff --git a/src/react-ui/languages/common/conversion.ts b/src/react-ui/languages/common/conversion.ts index 22e4f58b..e5ad80a4 100644 --- a/src/react-ui/languages/common/conversion.ts +++ b/src/react-ui/languages/common/conversion.ts @@ -25,6 +25,8 @@ export const 繁體轉簡體 = 字典 const sameConversionSet: Set = new Set([ '了', '出', + '别', + '千', '吃', '合', '同', @@ -39,6 +41,7 @@ const sameConversionSet: Set = new Set([ '系', '累', '致', + '荐', '面', ]) diff --git a/src/react-ui/languages/mandarin/songs/index.ts b/src/react-ui/languages/mandarin/songs/index.ts deleted file mode 100644 index 559b1624..00000000 --- a/src/react-ui/languages/mandarin/songs/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -const handleSongTreble = - (artist: string) => - ([load, name, video]: [ - () => Promise<{ lyrics: string[] }>, - string, - string, - ]) => ({ - artist, - lang: 'mandarin', - load, - name, - video, - }) - -const sandyLamSongs = [ - [ - () => import('./sandy-lam_zhi-shao-hai-you-ni'), - '至少還有你', - 'pQlAWZLOpgo', - ], -].map(handleSongTreble('Sandy Lam')) - -const stefanieSunSongs = [ - [() => import('./stefanie-sun_yu-jian'), '遇見', 'm4nu_F_9dWU'], -].map(handleSongTreble('Stefanie Sun')) - -const kayTseSongs = [ - [() => import('./kay-tse_di-er-ge-jia'), '第二個家', 'DMuCb0LGvtQ'], -].map(handleSongTreble('Kay Tse')) - -export const songs = kayTseSongs.concat(sandyLamSongs).concat(stefanieSunSongs) diff --git a/src/react-ui/languages/mandarin/songs/kay-tse_di-er-ge-jia.ts b/src/react-ui/languages/mandarin/songs/kay-tse_di-er-ge-jia.ts deleted file mode 100644 index 92c46789..00000000 --- a/src/react-ui/languages/mandarin/songs/kay-tse_di-er-ge-jia.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const lyrics = [ - '哪怕只是曇花, 也會一夜長大 小想法終究會發芽, 出發吧不要怕, 候鳥到達前沒有家 逆風中也優雅', - - '這一剎那看著同一片的晚霞, 你也像我嗎 懷疑過嗎, 理想的存在嗎 天使從不說話, 紙屑被吹起又落下', - - '因為你 我找到第二個家, 我是我是你也可以是他, 我把我的執著都放下, 心在哪天堂就在哪', - - '因為你 我找到第二個家, 我是我是你也可以是他, 我把我的盔甲都卸下, 然後更瀟灑', - - '時間會被風化, 為何還放不下 浪花也曾經是海沙, 混亂中有規劃, 有白鴿也會有烏鴉 有期待有驚訝', - - '這一剎那看著同一片的晚霞, 你也像我嗎 否定過嗎, 美好的存在嗎 天使從不回答, 有人說夢話在沙發', - - '因為你 我找到第二個家, 我是我是你也可以是他, 我把我的執著都放下, 我在哪不太重要吧', - - '因為你 我找到第二個家, 我是我是你也可以是他, 我把我的盔甲都卸下, 來把愛容納', -] diff --git a/src/react-ui/languages/mandarin/songs/sandy-lam_zhi-shao-hai-you-ni.ts b/src/react-ui/languages/mandarin/songs/sandy-lam_zhi-shao-hai-you-ni.ts deleted file mode 100644 index 22fdfb34..00000000 --- a/src/react-ui/languages/mandarin/songs/sandy-lam_zhi-shao-hai-you-ni.ts +++ /dev/null @@ -1,37 +0,0 @@ -export const lyrics = [ - '我怕來不及 我要抱著你 直到感覺你的皺紋 有了歲月的痕跡', - '直到肯定你是真的 直到失去力氣 為了你 我願意', - '動也不能動 也要看著你 直到感覺你的髮線 有了白雪的痕跡', - '直到視線變得模糊 直到不能呼吸 讓我們 形影不離', - - '如果 全世界我也可以放棄', - '至少還有你 值得我去珍惜', - '而你在這裡 就是生命的奇跡', - '也許 全世界我也可以忘記', - '就是不願意 失去你的消息', - '你掌心的痣 我總記得在那裡', - - '我怕來不及 我要抱著你 直到感覺你的髮線 有了白雪的痕跡', - '直到視線變得模糊 直到不能呼吸 讓我們 形影不離', - - '如果 全世界我也可以放棄', - '至少還有你 值得我去珍惜', - '而你在這裡 就是生命的奇跡', - '也許 全世界我也可以忘記', - '就是不願意 失去你的消息', - '你掌心的痣 我總記得在那裡', - - '我們好不容易 我們身不由己', - '我怕時間太快 不夠將你看仔細', - '我怕時間太慢 日夜擔心失去你', - '恨不得一夜之間白頭 永不分離', - - '如果 全世界我也可以放棄', - '至少還有你 值得我去珍惜', - '而你在這裡 就是生命的奇跡', - '也許 全世界我也可以忘記', - '就是不願意 失去你的消息', - '你掌心的痣 我總記得在那裡 在那裡', - - '在哪裡', -] diff --git a/src/react-ui/languages/mandarin/songs/stefanie-sun_yu-jian.ts b/src/react-ui/languages/mandarin/songs/stefanie-sun_yu-jian.ts deleted file mode 100644 index 8cf3de6a..00000000 --- a/src/react-ui/languages/mandarin/songs/stefanie-sun_yu-jian.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const lyrics = [ - '听见 冬天的离开, 我在某年某月 醒过来, 我想 我等 我期待, 未来却不能因此安排', - - '阴天 傍晚 车窗外, 未来有一个人在等待, 向左向右向前看, 爱要拐几个弯才来', - - '我遇见谁 会有怎样的对白, 我等的人 他在多远的未来, 我听见风来自地铁和人海, 我排着队 拿着爱的号码牌', - - '阴天 傍晚 车窗外, 未来有一个人在等待, 向左向右向前看, 爱要拐几个弯才来', - - '我遇见谁 会有怎样的对白, 我等的人 他在多远的未来, 我听见风来自地铁和人海, 我排着队 拿着爱的号码牌', - - '我往前飞 飞过一片时间海, 我们也曾在爱情里受伤害, 我看着路 梦的入口有点窄', - - '我遇见你是最美丽的意外, 总有一天 我的谜底会揭开', -] diff --git a/src/react-ui/lib/backendClient.ts b/src/react-ui/lib/backendClient.ts index 94a7ca47..021cbd66 100644 --- a/src/react-ui/lib/backendClient.ts +++ b/src/react-ui/lib/backendClient.ts @@ -1,4 +1,4 @@ -import { AnkiGql, Me, TextGql } from '../graphql/graphql' +import { AnkiGql, Me, SongGql, TextGql } from '../graphql/graphql' const baseURL = process.env.NODE_ENV === 'production' ? '/api' : 'http://localhost:9000' @@ -110,6 +110,59 @@ const getUserAnkis = (items: number, offset: number, query: string) => total: ankisTotal, })) +export type SongItem = Pick< + SongGql, + 'id' | 'artist' | 'language' | 'title' | 'lyrics' | 'videoUrl' +> + +const getSongs = ( + language: string, + query: string, + items: number, + offset: number, +) => + fetchGraphQL<{ + songs: SongItem[] + songsTotal: number + }>(`#graphql + query { + songs( + itemsNum: ${items}, + offset: ${offset}, + lang: "${language}", + query: ${query ? `"${query}"` : null} + ) { + id + artist + language + videoUrl + title + } + songsTotal( + lang: "${language}", + query: ${query ? `"${query}"` : null} + ) + } + `).then(({ songs, songsTotal }) => ({ + list: songs, + total: songsTotal, + })) + +const getSongLyrics = (id: number) => + fetchGraphQL<{ + song: Pick | null + }>(`#graphql + query { + song( + id: ${id} + ) { + lyrics + } + } + `).then(({ song }) => ({ + lyrics: song?.lyrics, + })) + const getAnkisRound = (query: string) => fetchGraphQL<{ ankisRound: Array> @@ -194,6 +247,8 @@ export const backendClient = { getAnkisRound, getHealth, getInfo, + getSongLyrics, + getSongs, getUserAnkis, getUserTexts, login,