- 新增阿弥陀经/普门品/道德经/清静经/太上感应篇(维基文库底本,OpenCC转简体) - 经典切换改为下拉框(佛/道分组),移动端复用底部面板并显示分组标题 - TTS 请求失败自动重试3次;普门品偈颂拆分修复600字上限问题 - 服务端缓存原子写(.tmp+rename),上限放宽至800字 - PWA:manifest + Service Worker,安装后无地址栏独立窗口 - 偏好记忆/逐字光标/回到诵读处/夜间模式等此前改动一并入库 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1422 lines
109 KiB
JavaScript
1422 lines
109 KiB
JavaScript
/**
|
||
* 心经 / 金刚经 诵读页前端逻辑:经文渲染、分页阅读、逐段高亮朗读、音色选择。
|
||
*/
|
||
|
||
// ===== 经文数据 =====
|
||
// segments 项:{ h: 品名(可选), g: 品名小注(可选), t: 正文 }
|
||
const SUTRAS = [
|
||
{
|
||
id: "xinjing",
|
||
name: "心经",
|
||
title: "般若波罗蜜多心经",
|
||
seal: "心经",
|
||
cat: "佛经",
|
||
translator: "唐三藏法师 玄奘 译",
|
||
source: "故宫博物院",
|
||
sourceUrl: "https://www.dpm.org.cn/lemmas/243222.html",
|
||
pageSize: 12, // 短篇一屏读完
|
||
segments: [
|
||
{ t: "观自在菩萨,行深般若波罗蜜多时,照见五蕴皆空,度一切苦厄。" },
|
||
{ t: "舍利子,色不异空,空不异色。色即是空,空即是色。受、想、行、识,亦复如是。" },
|
||
{ t: "舍利子,是诸法空相,不生不灭,不垢不净,不增不减。" },
|
||
{ t: "是故空中无色,无受、想、行、识;无眼、耳、鼻、舌、身、意;无色、声、香、味、触、法;" },
|
||
{ t: "无眼界,乃至无意识界;" },
|
||
{ t: "无无明,亦无无明尽,乃至无老死,亦无老死尽;" },
|
||
{ t: "无苦、集、灭、道;无智亦无得。" },
|
||
{ t: "以无所得故,菩提萨埵,依般若波罗蜜多故,心无挂碍。" },
|
||
{ t: "无挂碍故,无有恐怖,远离颠倒梦想,究竟涅槃。" },
|
||
{ t: "三世诸佛,依般若波罗蜜多故,得阿耨多罗三藐三菩提。" },
|
||
{ t: "故知般若波罗蜜多,是大神咒,是大明咒,是无上咒,是无等等咒,能除一切苦,真实不虚。" },
|
||
{ t: "故说般若波罗蜜多咒,即说咒曰:揭谛揭谛,波罗揭谛,波罗僧揭谛,菩提萨婆诃。" },
|
||
],
|
||
},
|
||
{
|
||
id: "jingang",
|
||
name: "金刚经",
|
||
title: "金刚般若波罗蜜经",
|
||
seal: "金刚经",
|
||
cat: "佛经",
|
||
translator: "姚秦三藏法师 鸠摩罗什 译",
|
||
source: "维基文库 · 大正新修大藏经",
|
||
sourceUrl: "https://zh.wikisource.org/wiki/金剛般若波羅蜜經_(鳩摩羅什)",
|
||
pageSize: 4, // 每页 4 段
|
||
segments: [
|
||
{ h: "法会因由分第一", g: "说法聚会,由此起因", t: "如是我闻。一时,佛在舍卫国祇树给孤独园,与大比丘众千二百五十人俱。尔时,世尊食时,著衣持钵,入舍卫大城乞食。于其城中,次第乞已,还至本处。饭食讫,收衣钵,洗足已,敷座而坐。" },
|
||
{ h: "善现启请分第二", g: "善现长老,启请佛训", t: "时,长老须菩提,在大众中,即从座起,偏袒右肩,右膝著地,合掌恭敬,而白佛言:“希有世尊!如来善护念诸菩萨,善付嘱诸菩萨。世尊!善男子、善女人,发阿耨多罗三藐三菩提心,应云何住?云何降伏其心?”佛言:“善哉,善哉。须菩提!如汝所说:如来善护念诸菩萨,善付嘱诸菩萨。汝今谛听,当为汝说:善男子、善女人,发阿耨多罗三藐三菩提心,应如是住,如是降伏其心。”“唯然,世尊!愿乐欲闻。”" },
|
||
{ h: "大乘正宗分第三", g: "最大之乘,最正之宗", t: "佛告须菩提:“诸菩萨摩诃萨,应如是降伏其心:所有一切众生之类,若卵生、若胎生、若湿生、若化生;若有色、若无色;若有想、若无想、若非有想非无想,我皆令入无馀涅槃而灭度之。如是灭度无量无数无边众生,实无众生得灭度者。何以故?须菩提!若菩萨有我相、人相、众生相、寿者相,即非菩萨。”" },
|
||
{ h: "妙行无住分第四", g: "奥妙之行,本无住著", t: "“复次,须菩提!菩萨于法,应无所住,行于布施。所谓不住色布施,不住声、香、味、触、法布施。须菩提!菩萨应如是布施,不住于相。何以故?若菩萨不住相布施,其福德不可思量。”“须菩提!于意云何?东方虚空,可思量不?”“不也,世尊!”“须菩提!南、西、北方,四维上下虚空,可思量不?”“不也,世尊!”“须菩提!菩萨无住相布施,福德亦复如是不可思量。须菩提!菩萨但应如所教住。”" },
|
||
{ h: "如理实见分第五", g: "自如之理,乃见真实", t: "“须菩提!于意云何?可以身相见如来不?”“不也,世尊!不可以身相得见如来。何以故?如来所说身相,即非身相。”佛告须菩提:“凡所有相,皆是虚妄。若见诸相非相,即见如来。”" },
|
||
{ h: "正信希有分第六", g: "生正信心,最为希有", t: "须菩提白佛言:“世尊!颇有众生,得闻如是言说章句,生实信不?”佛告须菩提:“莫作是说。如来灭后,后五百岁,有持戒修福者,于此章句能生信心,以此为实,当知是人不于一佛、二佛、三四五佛而种善根,已于无量千万佛所种诸善根,闻是章句,乃至一念生净信者,须菩提!如来悉知悉见,是诸众生得如是无量福德。何以故?是诸众生无复我相、人相、众生相、寿者相。”“无法相,亦无非法相。何以故?是诸众生若心取相,则为著我、人、众生、寿者。若取法相,即著我、人、众生、寿者。何以故?若取非法相,即著我、人、众生、寿者。是故不应取法,不应取非法。以是义故,如来常说:汝等比丘,知我说法,如筏喻者,法尚应舍,何况非法。”" },
|
||
{ h: "无得无说分第七", g: "空则无得,寂则无说", t: "“须菩提!于意云何?如来得阿耨多罗三藐三菩提耶?如来有所说法耶?”须菩提言:“如我解佛所说义,无有定法,名阿耨多罗三藐三菩提,亦无有定法,如来可说。何以故?如来所说法,皆不可取、不可说;非法,非非法。所以者何?一切贤圣,皆以无为法而有差别。”" },
|
||
{ h: "依法出生分第八", g: "诸佛之法,依此生出", t: "“须菩提!于意云何?若人满三千大千世界七宝以用布施,是人所得福德,宁为多不?”须菩提言:“甚多,世尊!何以故?是福德即非福德性,是故如来说福德多。”“若复有人,于此经中受持,乃至四句偈等,为他人说,其福胜彼。何以故?须菩提!一切诸佛,及诸佛阿耨多罗三藐三菩提法,皆从此经出。须菩提!所谓佛法者,即非佛法,是名佛法。”" },
|
||
{ h: "一相无相分第九", g: "只此一相,本来无形", t: "“须菩提!于意云何?须陀洹能作是念:我得须陀洹果不?”须菩提言:“不也,世尊!何以故?须陀洹名为入流,而无所入;不入色、声、香、味、触、法,是名须陀洹。”“须菩提!于意云何?斯陀含能作是念:我得斯陀含果不?”须菩提言:“不也,世尊!何以故?斯陀含名一往来,而实无往来,是名斯陀含。”“须菩提!于意云何?阿那含能作是念:我得阿那含果不?”须菩提言:“不也,世尊!何以故?阿那含名为不来,而实无不来,是故名阿那含。”“须菩提!于意云何?阿罗汉能作是念:我得阿罗汉道不?”须菩提言:“不也,世尊!何以故?实无有法名阿罗汉。世尊!若阿罗汉作是念:我得阿罗汉道,即为著我、人、众生、寿者。世尊!佛说我得无诤三昧,人中最为第一,是第一离欲阿罗汉。世尊!我不作是念:我是离欲阿罗汉。世尊!我若作是念:我得阿罗汉道,世尊则不说须菩提是乐阿兰那行者。以须菩提实无所行,而名须菩提,是乐阿兰那行。”" },
|
||
{ h: "庄严净土分第十", g: "成就庄严,净明心地", t: "佛告须菩提:“于意云何?如来昔在燃灯佛所,于法有所得不?”“不也,世尊!如来在燃灯佛所,于法实无所得。”“须菩提!于意云何?菩萨庄严佛土不?”“不也,世尊!何以故?庄严佛土者,即非庄严,是名庄严。”“是故,须菩提!诸菩萨摩诃萨,应如是生清净心,不应住色生心,不应住声、香、味、触、法生心,应无所住而生其心。”“须菩提!譬如有人,身如须弥山王,于意云何?是身为大不?”须菩提言:“甚大,世尊!何以故?佛说非身,是名大身。”" },
|
||
{ h: "无为福胜分第十一", g: "修无为福,胜于布施", t: "“须菩提!如恒河中所有沙数,如是沙等恒河,于意云何?是诸恒河沙,宁为多不?”须菩提言:“甚多,世尊!但诸恒河,尚多无数,何况其沙。”“须菩提!我今实言告汝:若有善男子、善女人,以七宝满尔所恒河沙数三千大千世界,以用布施,得福多不?”须菩提言:“甚多,世尊!”佛告须菩提:“若善男子、善女人,于此经中,乃至受持四句偈等,为他人说,而此福德,胜前福德。”" },
|
||
{ h: "尊重正教分第十二", g: "受持正教,天人尊重", t: "“复次,须菩提!随说是经,乃至四句偈等,当知此处,一切世间,天、人、阿修罗,皆应供养,如佛塔庙。何况有人,尽能受持读诵。须菩提!当知是人成就最上第一希有之法,若是经典所在之处,即为有佛,若尊重弟子。”" },
|
||
{ h: "如法受持分第十三", g: "当如此法,承受奉持", t: "尔时,须菩提白佛言:“世尊!当何名此经?我等云何奉持?”佛告须菩提:“是经名为金刚般若波罗蜜,以是名字,汝当奉持。所以者何?须菩提!佛说般若波罗蜜,即非般若波罗蜜,是名般若波罗蜜。须菩提!于意云何?如来有所说法不?”须菩提白佛言:“世尊!如来无所说。”“须菩提!于意云何?三千大千世界所有微尘,是为多不?”须菩提言:“甚多,世尊!”“须菩提!诸微尘,如来说非微尘,是名微尘。如来说世界,非世界,是名世界。须菩提!于意云何?可以三十二相见如来不?”“不也,世尊!不可以三十二相得见如来。何以故?如来说三十二相,即是非相,是名三十二相。”“须菩提!若有善男子、善女人,以恒河沙等身命布施;若复有人,于此经中,乃至受持四句偈等,为他人说,其福甚多!”" },
|
||
{ h: "离相寂灭分第十四", g: "离诸形相,自得寂灭", t: "尔时,须菩提闻说是经,深解义趣,涕泪悲泣,而白佛言:“希有世尊!佛说如是甚深经典,我从昔来所得慧眼,未曾得闻如是之经。世尊!若复有人,得闻是经,信心清净,则生实相,当知是人,成就第一希有功德。世尊!是实相者,则是非相,是故如来说名实相。世尊!我今得闻如是经典,信解受持,不足为难;若当来世,后五百岁,其有众生,得闻是经,信解受持,是人则为第一希有。何以故?此人无我相、无人相、无众生相、无寿者相。所以者何?我相,即是非相;人相、众生相、寿者相,即是非相。何以故?离一切诸相,则名诸佛。”" },
|
||
{ t: "佛告须菩提:“如是,如是!若复有人,得闻是经,不惊、不怖、不畏,当知是人,甚为希有。何以故?须菩提!如来说:第一波罗蜜,即非第一波罗蜜,是名第一波罗蜜。须菩提!忍辱波罗蜜,如来说非忍辱波罗蜜,是名忍辱波罗蜜。何以故?须菩提!如我昔为歌利王割截身体,我于尔时,无我相、无人相、无众生相、无寿者相。何以故?我于往昔,节节支解时,若有我相、人相、众生相、寿者相,应生瞋恨。须菩提!又念过去,于五百世,作忍辱仙人,于尔所世,无我相、无人相、无众生相、无寿者相。是故须菩提!菩萨应离一切相,发阿耨多罗三藐三菩提心。不应住色生心,不应住声、香、味、触、法生心,应生无所住心。若心有住,则为非住。”" },
|
||
{ t: "“是故佛说:菩萨心不应住色布施。须菩提!菩萨为利益一切众生故,应如是布施。如来说:一切诸相,即是非相。又说:一切众生,即非众生。须菩提!如来是真语者、实语者、如语者、不诳语者、不异语者。”“须菩提!如来所得法,此法无实无虚。须菩提!若菩萨心住于法而行布施,如人入暗,则无所见;若菩萨心不住法而行布施,如人有目,日光明照,见种种色。”“须菩提!当来之世,若有善男子、善女人,能于此经,受持读诵,则为如来,以佛智慧,悉知是人,悉见是人,皆得成就无量无边功德。”" },
|
||
{ h: "持经功德分第十五", g: "受持此经,功德无量", t: "“须菩提!若有善男子、善女人,初日分以恒河沙等身布施,中日分复以恒河沙等身布施,后日分亦以恒河沙等身布施,如是无量百千万亿劫,以身布施;若复有人,闻此经典,信心不逆,其福胜彼,何况书写、受持、读诵、为人解说。”“须菩提!以要言之,是经有不可思议、不可称量、无边功德。如来为发大乘者说,为发最上乘者说。若有人能受持读诵,广为人说,如来悉知是人,悉见是人,皆得成就不可量、不可称、无有边、不可思议功德。”“如是人等,则为荷担如来阿耨多罗三藐三菩提。何以故?须菩提!若乐小法者,著我见、人见、众生见、寿者见,则于此经不能听受、读诵,为人解说。”“须菩提!在在处处,若有此经,一切世间,天、人、阿修罗,所应供养;当知此处,则为是塔,皆应恭敬,作礼围绕,以诸华香而散其处。”" },
|
||
{ h: "能净业障分第十六", g: "若能清净,业障尽消", t: "“复次,须菩提!善男子、善女人,受持读诵此经,若为人轻贱,是人先世罪业,应堕恶道,以今世人轻贱故,先世罪业则为消灭,当得阿耨多罗三藐三菩提。”“须菩提!我念过去无量阿僧祇劫,于燃灯佛前,得值八百四千万亿那由他诸佛,悉皆供养承事,无空过者;若复有人,于后末世,能受持读诵此经,所得功德,于我所供养诸佛功德,百分不及一,千万亿分、乃至算数譬喻所不能及。”“须菩提!若善男子、善女人,于后末世,有受持读诵此经,所得功德,我若具说者,或有人闻,心则狂乱,狐疑不信。须菩提!当知是经义不可思议,果报亦不可思议!”" },
|
||
{ h: "究竟无我分第十七", g: "成佛究竟,本无我相", t: "尔时,须菩提白佛言:“世尊!善男子、善女人,发阿耨多罗三藐三菩提心,云何应住?云何降伏其心?”佛告须菩提:“善男子、善女人,发阿耨多罗三藐三菩提心者,当生如是心:我应灭度一切众生。灭度一切众生已,而无有一众生实灭度者。何以故?须菩提!若菩萨有我相、人相、众生相、寿者相,则非菩萨。所以者何?须菩提!实无有法,发阿耨多罗三藐三菩提心者。”“须菩提!于意云何?如来于燃灯佛所,有法得阿耨多罗三藐三菩提不?”“不也,世尊!如我解佛所说义,佛于燃灯佛所,无有法得阿耨多罗三藐三菩提。”" },
|
||
{ t: "佛言:“如是,如是!须菩提!实无有法,如来得阿耨多罗三藐三菩提。须菩提!若有法,如来得阿耨多罗三藐三菩提者,燃灯佛则不与我授记:‘汝于来世,当得作佛,号释迦牟尼。’以实无有法得阿耨多罗三藐三菩提,是故燃灯佛与我授记,作是言:‘汝于来世,当得作佛,号释迦牟尼。’何以故?如来者,即诸法如义。”“若有人言:如来得阿耨多罗三藐三菩提。须菩提!实无有法,佛得阿耨多罗三藐三菩提。须菩提!如来所得阿耨多罗三藐三菩提,于是中无实无虚。是故如来说:一切法皆是佛法。须菩提!所言一切法者,即非一切法,是故名一切法。”" },
|
||
{ t: "“须菩提!譬如人身长大。”须菩提言:“世尊!如来说:人身长大,则为非大身,是名大身。”“须菩提!菩萨亦如是。若作是言:我当灭度无量众生,则不名菩萨。何以故?须菩提!实无有法名为菩萨。是故佛说一切法,无我、无人、无众生、无寿者。”“须菩提!若菩萨作是言:‘我当庄严佛土’,是不名菩萨。何以故?如来说:庄严佛土者,即非庄严,是名庄严。”“须菩提!若菩萨通达无我、法者,如来说名真是菩萨。”" },
|
||
{ h: "一体同观分第十八", g: "万法归一,更无异观", t: "“须菩提!于意云何?如来有肉眼不?”“如是,世尊!如来有肉眼。”“须菩提!于意云何?如来有天眼不?”“如是,世尊!如来有天眼。”“须菩提!于意云何?如来有慧眼不?”“如是,世尊!如来有慧眼。”“须菩提!于意云何?如来有法眼不?”“如是,世尊!如来有法眼。”“须菩提!于意云何?如来有佛眼不?”“如是,世尊!如来有佛眼。”“须菩提!于意云何?如恒河中所有沙,佛说是沙不?”“如是,世尊!如来说是沙。”“须菩提!于意云何?如一恒河中所有沙,有如是沙等恒河,是诸恒河所有沙数,佛世界如是,宁为多不?”“甚多,世尊。”佛告须菩提:“尔所国土中,所有众生,若干种心,如来悉知。何以故?如来说:诸心皆为非心,是名为心。所以者何?须菩提!过去心不可得,现在心不可得,未来心不可得。”" },
|
||
{ h: "法界通化分第十九", g: "法身遍界,通化无边", t: "“须菩提!于意云何?若有人满三千大千世界七宝,以用布施,是人以是因缘,得福多不?”“如是,世尊!此人以是因缘,得福甚多。”“须菩提!若福德有实,如来不说得福德多;以福德无故,如来说得福德多。”" },
|
||
{ h: "离色离相分第二十", g: "色相皆妄,离妄见性", t: "“须菩提!于意云何?佛可以具足色身见不?”“不也,世尊!如来不应以具足色身见。何以故?如来说具足色身,即非具足色身,是名具足色身。”“须菩提!于意云何?如来可以具足诸相见不?”“不也,世尊!如来不应以具足诸相见。何以故?如来说诸相具足,即非具足,是名诸相具足。”" },
|
||
{ h: "非说所说分第二十一", g: "法无可说,所说非法", t: "“须菩提!汝勿谓如来作是念:我当有所说法。莫作是念。何以故?若人言如来有所说法,即为谤佛,不能解我所说故。须菩提!说法者,无法可说,是名说法。”尔时,慧命须菩提白佛言:“世尊!颇有众生,于未来世,闻说是法,生信心不?”佛言:“须菩提!彼非众生,非不众生。何以故?须菩提!众生众生者,如来说非众生,是名众生。”" },
|
||
{ h: "无法可得分第二十二", g: "悟性空故,无法可得", t: "须菩提白佛言:“世尊!佛得阿耨多罗三藐三菩提,为无所得耶?”佛言:“如是,如是。须菩提!我于阿耨多罗三藐三菩提乃至无有少法可得,是名阿耨多罗三藐三菩提。”" },
|
||
{ h: "净心行善分第二十三", g: "以清净心,行诸善法", t: "“复次,须菩提!是法平等,无有高下,是名阿耨多罗三藐三菩提;以无我、无人、无众生、无寿者,修一切善法,则得阿耨多罗三藐三菩提。须菩提!所言善法者,如来说即非善法,是名善法。”" },
|
||
{ h: "福智无比分第二十四", g: "福智甚大,无物可比", t: "“须菩提!若三千大千世界中,所有诸须弥山王,如是等七宝聚,有人持用布施;若人以此般若波罗蜜经,乃至四句偈等,受持读诵,为他人说,于前福德百分不及一,百千万亿分、乃至算数譬喻所不能及。”" },
|
||
{ h: "化无所化分第二十五", g: "圣凡同性,化无所化", t: "“须菩提!于意云何?汝等勿谓如来作是念:我当度众生。须菩提!莫作是念。何以故?实无有众生如来度者。若有众生如来度者,如来则有我、人、众生、寿者。须菩提!如来说有我者,则非有我,而凡夫之人以为有我。须菩提!凡夫者,如来说则非凡夫,是名凡夫。”" },
|
||
{ h: "法身非相分第二十六", g: "清净法身,非属相貌", t: "“须菩提!于意云何?可以三十二相观如来不?”须菩提言:“如是!如是!以三十二相观如来。”佛言:“须菩提!若以三十二相观如来者,转轮圣王则是如来。”须菩提白佛言:“世尊!如我解佛所说义,不应以三十二相观如来。”尔时,世尊而说偈言:“若以色见我,以音声求我,是人行邪道,不能见如来。”" },
|
||
{ h: "无断无灭分第二十七", g: "依法修持,不应断灭", t: "“须菩提!汝若作是念:如来不以具足相故,得阿耨多罗三藐三菩提。须菩提!莫作是念:如来不以具足相故,得阿耨多罗三藐三菩提。”“须菩提!汝若作是念,发阿耨多罗三藐三菩提心者,说诸法断灭。莫作是念!何以故?发阿耨多罗三藐三菩提心者,于法不说断灭相。”" },
|
||
{ h: "不受不贪分第二十八", g: "一尘不染,何贪何受", t: "“须菩提!若菩萨以满恒河沙等世界七宝持用布施;若复有人,知一切法无我,得成于忍,此菩萨胜前菩萨所得功德。何以故?须菩提!以诸菩萨不受福德故。”须菩提白佛言:“世尊!云何菩萨不受福德?”“须菩提!菩萨所作福德,不应贪著,是故说不受福德。”" },
|
||
{ h: "威仪寂静分第二十九", g: "真性寂静,不假威仪", t: "“须菩提!若有人言:如来若来若去,若坐若卧,是人不解我所说义。何以故?如来者,无所从来,亦无所去,故名如来。”" },
|
||
{ h: "一合理相分第三十", g: "一合之理,实无有相", t: "“须菩提!若善男子、善女人,以三千大千世界碎为微尘,于意云何?是微尘众宁为多不?”须菩提言:“甚多,世尊!何以故?若是微尘众实有者,佛则不说是微尘众。所以者何?佛说微尘众,则非微尘众,是名微尘众。”“世尊!如来所说三千大千世界,则非世界,是名世界。何以故?若世界实有者,则是一合相。如来说一合相,则非一合相,是名一合相。”“须菩提!一合相者,则是不可说;但凡夫之人,贪著其事。”" },
|
||
{ h: "知见不生分第三十一", g: "如此知见,法相不生", t: "“须菩提!若人言:佛说我见、人见、众生见、寿者见。须菩提!于意云何?是人解我所说义不?”“不也,世尊!是人不解如来所说义。何以故?世尊说我见、人见、众生见、寿者见,即非我见、人见、众生见、寿者见,是名我见、人见、众生见、寿者见。”“须菩提!发阿耨多罗三藐三菩提心者,于一切法,应如是知,如是见,如是信解,不生法相。须菩提!所言法相者,如来说即非法相,是名法相。”" },
|
||
{ h: "应化非真分第三十二", g: "应现设化,亦非真实", t: "“须菩提!若有人以满无量阿僧祇世界七宝,持用布施;若有善男子、善女人,发菩提心者,持于此经,乃至四句偈等,受持读诵,为人演说,其福胜彼。云何为人演说?不取于相,如如不动。何以故?一切有为法,如梦幻泡影,如露亦如电,应作如是观。”佛说是经已,长老须菩提及诸比丘、比丘尼、优婆塞、优婆夷、一切世间天、人、阿修罗,闻佛所说,皆大欢喜,信受奉行。" },
|
||
],
|
||
},
|
||
{
|
||
id: "amitabha",
|
||
name: "阿弥陀经",
|
||
title: "佛说阿弥陀经",
|
||
seal: "弥陀经",
|
||
cat: "佛经",
|
||
translator: "姚秦三藏法师 鸠摩罗什 译",
|
||
source: "维基文库 · 大正新修大藏经",
|
||
sourceUrl: "https://zh.wikisource.org/wiki/佛說阿彌陀經",
|
||
pageSize: 5, // 每页 5 段
|
||
segments: [
|
||
{ t: "如是我闻:一时,佛在舍卫国衹树给孤独园,与大比丘僧千二百五十人俱,皆是大阿罗汉,众所知识。长老舍利弗、摩诃目犍连、摩诃迦叶、摩诃迦旃延、摩诃俱𫄨罗、离婆多、周利槃陀伽、难陀、阿难陀、罗睺罗、憍梵波提、宾头卢颇罗堕、迦留陀夷、摩诃劫宾那、薄拘罗、阿那楼驮,如是等诸大弟子。并诸菩萨摩诃萨——文殊师利法王子、阿逸多菩萨、干陀诃提菩萨、常精进菩萨——与如是等诸大菩萨,及释提桓因等无量诸天大众俱。" },
|
||
{ t: "尔时,佛告长老舍利弗:「从是西方过十万亿佛土,有世界名曰极乐。其土有佛,号阿弥陀,今现在说法。舍利弗!彼土何故名为极乐?其国众生无有众苦,但受诸乐,故名极乐。又舍利弗!极乐国土,七重栏楯、七重罗网、七重行树,皆是四宝周匝围绕,是故彼国名为极乐。" },
|
||
{ t: "又舍利弗!极乐国土有七宝池,八功德水充满其中,池底纯以金沙布地。四边阶道,金、银、琉璃、玻璃合成;上有楼阁,亦以金、银、琉璃、玻璃、砗磲、赤珠、玛瑙而严饰之。池中莲华,大如车轮,青色青光,黄色黄光,赤色赤光,白色白光,微妙香洁。舍利弗!极乐国土成就如是功德庄严。" },
|
||
{ t: "又舍利弗!彼佛国土,常作天乐,黄金为地,昼夜六时,雨天曼陀罗华。其土众生,常以清旦,各以衣裓盛众妙华,供养他方十万亿佛;即以食时,还到本国,饭食经行。舍利弗!极乐国土成就如是功德庄严。" },
|
||
{ t: "复次舍利弗!彼国常有种种奇妙杂色之鸟——白鹤、孔雀、鹦鹉、舍利、迦陵频伽、共命之鸟。是诸众鸟,昼夜六时出和雅音,其音演畅五根、五力、七菩提分、八圣道分如是等法。其土众生闻是音已,皆悉念佛、念法、念僧。舍利弗!汝勿谓此鸟实是罪报所生。所以者何?彼佛国土无三恶道。舍利弗!其佛国土尚无三恶道之名,何况有实?是诸众鸟,皆是阿弥陀佛欲令法音宣流,变化所作。舍利弗!彼佛国土微风吹动,诸宝行树及宝罗网出微妙音,譬如百千种乐同时俱作,闻是音者,自然皆生念佛念法念僧之心。舍利弗!其佛国土成就如是功德庄严。" },
|
||
{ t: "舍利弗,于汝意云何?彼佛何故号阿弥陀?舍利弗!彼佛光明无量,照十方国无所障碍,是故号为阿弥陀。又舍利弗!彼佛寿命及其人民,无量无边阿僧祇劫,故名阿弥陀。舍利弗!阿弥陀佛成佛以来,于今十劫。又舍利弗!彼佛有无量无边声闻弟子,皆阿罗汉,非是算数之所能知;诸菩萨众,亦复如是。舍利弗!彼佛国土成就如是功德庄严。" },
|
||
{ t: "又舍利弗!极乐国土众生生者,皆是阿鞞跋致。其中多有一生补处,其数甚多,非是算数所能知之,但可以无量无边阿僧祇说。舍利弗!众生闻者,应当发愿,愿生彼国。所以者何?得与如是诸上善人俱会一处。" },
|
||
{ t: "舍利弗!不可以少善根福德因缘得生彼国。舍利弗!若有善男子、善女人,闻说阿弥陀佛,执持名号,若一日、若二日、若三日、若四日、若五日、若六日、若七日,一心不乱。其人临命终时,阿弥陀佛与诸圣众,现在其前。是人终时,心不颠倒,即得往生阿弥陀佛极乐国土。舍利弗!我见是利,故说此言。若有众生闻是说者,应当发愿生彼国土。" },
|
||
{ t: "舍利弗!如我今者,赞叹阿弥陀佛不可思议功德之利;东方亦有阿閦鞞佛、须弥相佛、大须弥佛、须弥光佛、妙音佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||
{ t: "舍利弗!南方世界有日月灯佛、名闻光佛、大燄肩佛、须弥灯佛、无量精进佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||
{ t: "舍利弗!西方世界有无量寿佛、无量相佛、无量幢佛、大光佛、大明佛、宝相佛、净光佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||
{ t: "舍利弗!北方世界有燄肩佛、最胜音佛、难沮佛、日生佛、网明佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||
{ t: "舍利弗!下方世界有师子佛、名闻佛、名光佛、达摩佛、法幢佛、持法佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||
{ t: "舍利弗!上方世界有梵音佛、宿王佛、香上佛、香光佛、大燄肩佛、杂色宝华严身佛、娑罗树王佛、宝华德佛、见一切义佛、如须弥山佛,如是等恒河沙数诸佛,各于其国出广长舌相,遍覆三千大千世界,说诚实言:『汝等众生,当信是称赞不可思议功德一切诸佛所护念经。』" },
|
||
{ t: "舍利弗!于汝意云何?何故名为『一切诸佛所护念经』?舍利弗!若有善男子、善女人,闻是经受持者,及闻诸佛名者;是诸善男子、善女人,皆为一切诸佛之所护念,皆得不退转于阿耨多罗三藐三菩提。是故舍利弗!汝等皆当信受我语及诸佛所说。" },
|
||
{ t: "舍利弗!若有人已发愿、今发愿、当发愿,欲生阿弥陀佛国者;是诸人等,皆得不退转于阿耨多罗三藐三菩提,于彼国土若已生、若今生、若当生。是故舍利弗!诸善男子、善女人若有信者,应当发愿生彼国土。" },
|
||
{ t: "舍利弗!如我今者,称赞诸佛不可思议功德;彼诸佛等,亦称赞我不可思议功德,而作是言:『释迦牟尼佛能为甚难希有之事,能于娑婆国土五浊恶世,劫浊、见浊、烦恼浊、众生浊、命浊中,得阿耨多罗三藐三菩提,为诸众生说是一切世间难信之法。』舍利弗!当知我于五浊恶世行此难事,得阿耨多罗三藐三菩提,为一切世间说此难信之法,是为甚难!」" },
|
||
{ t: "佛说此经已,舍利弗及诸比丘,一切世间天、人、阿修罗等,闻佛所说,欢喜信受,作礼而去。" },
|
||
],
|
||
},
|
||
{
|
||
id: "pumen",
|
||
name: "普门品",
|
||
title: "观世音菩萨普门品",
|
||
seal: "普门品",
|
||
cat: "佛经",
|
||
translator: "姚秦鸠摩罗什 译长行 · 隋阇那崛多 译重颂",
|
||
source: "维基文库 · 大正新修大藏经",
|
||
sourceUrl: "https://zh.wikisource.org/wiki/妙法蓮華經觀世音菩薩普門品經",
|
||
pageSize: 6, // 每页 6 段
|
||
segments: [
|
||
{ h: "妙法莲华经观世音菩萨普门品第二十五", g: "姚秦鸠摩罗什译长行 · 隋阇那崛多译重颂", t: "尔时,无尽意菩萨即从座起,偏袒右肩,合掌向佛,而作是言:「世尊!观世音菩萨,以何因缘名观世音?」" },
|
||
{ t: "佛告无尽意菩萨:「善男子!若有无量百千万亿众生受诸苦恼,闻是观世音菩萨,一心称名,观世音菩萨即时观其音声,皆得解脱。若有持是观世音菩萨名者,设入大火,火不能烧,由是菩萨威神力故。若为大水所漂,称其名号,即得浅处。若有百千万亿众生,为求金、银、琉璃、砗磲、玛瑙、珊瑚、琥珀、真珠等宝,入于大海,假使黑风吹其船舫,飘堕罗刹鬼国,其中若有,乃至一人,称观世音菩萨名者,是诸人等皆得解脱罗刹之难。以是因缘,名观世音。若复有人临当被害,称观世音菩萨名者,彼所执刀杖寻段段坏,而得解脱。若三千大千国土,满中夜叉、罗刹,欲来恼人,闻其称观世音菩萨名者,是诸恶鬼,尚不能以恶眼视之,况复加害。设复有人,若有罪、若无罪,杻械、枷锁检系其身,称观世音菩萨名者,皆悉断坏,即得解脱。若三千大千国土,满中怨贼,有一商主,将诸商人,赍持重宝、经过险路,其中一人作是唱言:『诸善男子!勿得恐怖,汝等应当一心称观世音菩萨名号。是菩萨能以无畏施于众生,汝等若称名者,于此怨贼当得解脱。』众商人闻,俱发声言:『南无观世音菩萨。』称其名故,即得解脱。" },
|
||
{ t: "「无尽意!观世音菩萨摩诃萨,威神之力巍巍如是。若有众生多于淫欲,常念恭敬观世音菩萨,便得离欲。若多瞋恚,常念恭敬观世音菩萨,便得离瞋。若多愚痴,常念恭敬观世音菩萨,便得离痴。" },
|
||
{ t: "「无尽意!观世音菩萨有如是等大威神力,多所饶益,是故众生常应心念。若有女人,设欲求男,礼拜供养观世音菩萨,便生福德智慧之男;设欲求女,便生端正有相之女。宿殖德本,众人爱敬。" },
|
||
{ t: "「无尽意!观世音菩萨有如是力,若有众生,恭敬礼拜观世音菩萨,福不唐捐,是故众生皆应受持观世音菩萨名号。" },
|
||
{ t: "「无尽意!若有人受持六十二亿恒河沙菩萨名字,复尽形供养饮食、衣服、卧具、医药。于汝意云何?是善男子、善女人,功德多不?」" },
|
||
{ t: "无尽意言:「甚多,世尊!」" },
|
||
{ t: "佛言:「若复有人受持观世音菩萨名号,乃至一时礼拜、供养,是二人福,正等无异,于百千万亿劫不可穷尽。无尽意!受持观世音菩萨名号,得如是无量无边福德之利。」" },
|
||
{ t: "无尽意菩萨白佛言:「世尊!观世音菩萨,云何游此娑婆世界?云何而为众生说法?方便之力,其事云何?」" },
|
||
{ t: "佛告无尽意菩萨:「善男子!若有国土众生,应以佛身得度者,观世音菩萨即现佛身而为说法;应以辟支佛身得度者,即现辟支佛身而为说法;应以声闻身得度者,即现声闻身而为说法;应以梵王身得度者,即现梵王身而为说法;应以帝释身得度者,即现帝释身而为说法;应以自在天身得度者,即现自在天身而为说法;应以大自在天身得度者,即现大自在天身而为说法;应以天大将军身得度者,即现天大将军身而为说法;应以毗沙门身得度者,即现毗沙门身而为说法;应以小王身得度者,即现小王身而为说法;应以长者身得度者,即现长者身而为说法;应以居士身得度者,即现居士身而为说法;应以宰官身得度者,即现宰官身而为说法;应以婆罗门身得度者,即现婆罗门身而为说法;应以比丘、比丘尼、优婆塞、优婆夷身得度者,即现比丘、比丘尼、优婆塞、优婆夷身而为说法;应以长者、居士、宰官、婆罗门妇女身得度者,即现妇女身而为说法;应以童男、童女身得度者,即现童男、童女身而为说法;应以天、龙、夜叉、干闼婆、阿修罗、迦楼罗、紧那罗、摩睺罗伽、人非人等身得度者,即皆现之而为说法;应以执金刚身得度者,即现执金刚身而为说法。" },
|
||
{ t: "「无尽意!是观世音菩萨成就如是功德,以种种形,游诸国土,度脱众生。是故汝等,应当一心供养观世音菩萨。是观世音菩萨摩诃萨,于怖畏急难之中能施无畏,是故此娑婆世界,皆号之为施无畏者。」" },
|
||
{ t: "无尽意菩萨白佛言:「世尊!我今当供养观世音菩萨。」即解颈众宝珠、璎珞,价值百千两金,而以与之,作是言:「仁者!受此法施珍宝璎珞。」时观世音菩萨不肯受之。无尽意复白观世音菩萨言:「仁者!愍我等故,受此璎珞。」" },
|
||
{ t: "尔时佛告观世音菩萨:「当愍此无尽意菩萨及四众,天、龙、夜叉、干闼婆、阿修罗、迦楼罗、紧那罗、摩睺罗伽、人非人等故,受是璎珞。」" },
|
||
{ t: "即时观世音菩萨愍诸四众,及于天、龙、人非人等,受其璎珞,分作二分,一分奉释迦牟尼佛,一分奉多宝佛塔。" },
|
||
{ t: "「无尽意,观世音菩萨有如是自在神力,游于娑婆世界。」" },
|
||
{ t: "尔时无尽意菩萨以偈问曰:" },
|
||
{ t: "世尊妙相具,我今重问彼,佛子何因缘,名为观世音。具足妙相尊,偈答无尽意。汝听观音行,善应诸方所,弘誓深如海,历劫不思议,侍多千亿佛,发大清净愿。我为汝略说,闻名及见身,心念不空过,能灭诸有苦。假使兴害意,推落大火坑,念彼观音力,火坑变成池。或漂流巨海,龙鱼诸鬼难,念彼观音力,波浪不能没。或在须弥峰、为人所推堕,念彼观音力,如日虚空住。或被恶人逐,堕落金刚山,念彼观音力,不能损一毛。或值怨贼绕,各执刀加害,念彼观音力,咸即起慈心。或遭王难苦,临刑欲寿终,念彼观音力,刀寻段段坏。或囚禁枷锁,手足被杻械,念彼观音力,释然得解脱。咒诅诸毒药、所欲害身者,念彼观音力,还著于本人。或遇恶罗刹、毒龙诸鬼等,念彼观音力,时悉不敢害。若恶兽围绕,利牙爪可怖,念彼观音力,疾走无边方。蚖蛇及蝮蝎,气毒烟火燃,念彼观音力,寻声自回去。云雷鼓掣电,降雹澍大雨,念彼观音力,应时得消散。" },
|
||
{ t: "众生被困厄,无量苦逼身,观音妙智力,能救世间苦。具足神通力,广修智方便,十方诸国土,无刹不现身。种种诸恶趣,地狱鬼畜生,生老病死苦,以渐悉令灭。真观清净观,广大智慧观,悲观及慈观,常愿常瞻仰。无垢清净光、慧日破诸暗,能伏灾风火,普明照世间。悲体戒雷震,慈意妙大云,澍甘露法雨,灭除烦恼焰。诤讼经官处,怖畏军阵中,念彼观音力,众怨悉退散。妙音观世音、梵音海潮音,胜彼世间音,是故须常念,念念勿生疑。观世音净圣,于苦恼死厄、能为作依怙。具一切功德,慈眼视众生,福聚海无量,是故应顶礼。" },
|
||
{ t: "尔时持地菩萨即从座起,前白佛言:「世尊!若有众生,闻是观世音菩萨品自在之业,普门示现神通力者,当知是人功德不少。」" },
|
||
{ t: "佛说是普门品时,众中八万四千众生,皆发无等等阿耨多罗三藐三菩提心。" },
|
||
],
|
||
},
|
||
{
|
||
id: "daodejing",
|
||
name: "道德经",
|
||
title: "道德经",
|
||
seal: "道德经",
|
||
cat: "道家",
|
||
translator: "春秋 · 老子 著(王弼本)",
|
||
source: "维基文库",
|
||
sourceUrl: "https://zh.wikisource.org/wiki/道德經_(王弼本)",
|
||
pageSize: 9, // 81 章,每页 9 章
|
||
segments: [
|
||
{ h: "第一章", t: "道可道,非常道,名可名,非常名;无名,天地之始,有名,万物之母。故常无欲,以观其妙,常有欲,以观其徼;此两者,同出而异名,同谓之玄。玄之又玄,众妙之门。" },
|
||
{ h: "第二章", t: "天下皆知美之为美,斯恶已。皆知善之为善,斯不善已。故有无相生,难易相成,长短相较,高下相倾,音声相和,前后相随。是以圣人处无为之事,行不言之教;万物作焉而不辞,生而不有,为而不恃,功成而弗居。夫唯弗居,是以不去。" },
|
||
{ h: "第三章", t: "不尚贤,使民不争;不贵难得之货,使民不为盗;不见可欲,使民心不乱。是以圣人之治,虚其心,实其腹,弱其志,强其骨。常使民无知无欲。使夫智者不敢为也。为无为,则无不治。" },
|
||
{ h: "第四章", t: "道冲而用之或不盈,渊兮似万物之宗;挫其锐,解其纷,和其光,同其尘,湛兮似或存。吾不知谁之子,象帝之先。" },
|
||
{ h: "第五章", t: "天地不仁,以万物为刍狗;圣人不仁,以百姓为刍狗。天地之间,其犹橐籥乎?虚而不屈,动而愈出。多言数穷,不如守中。" },
|
||
{ h: "第六章", t: "谷神不死,是谓玄牝。玄牝之门,是谓天地根。緜緜若存,用之不勤。" },
|
||
{ h: "第七章", t: "天长地久。天地所以能长且久者,以其不自生,故能长生。是以圣人后其身而身先;外其身而身存。非以其无私邪,故能成其私。" },
|
||
{ h: "第八章", t: "上善若水。水善利万物而不争,处众人之所恶,故几于道。居善地,心善渊,与善仁,言善信,正善治,事善能,动善时。夫唯不争,故无尤。" },
|
||
{ h: "第九章", t: "持而盈之,不如其已;揣而棁之,不可长保。金玉满堂,莫之能守;富贵而骄,自遗其咎。功遂身退,天之道。" },
|
||
{ h: "第十章", t: "载营魄抱一,能无离乎?专气致柔,能婴儿乎?涤除玄览,能无疵乎?爱国治民,能无知乎?天门开阖,能为雌乎?明白四达,能无为乎?生之,畜之。生而不有,为而不恃,长而不宰,是谓玄德。" },
|
||
{ h: "第十一章", t: "三十辐共一毂,当其无,有车之用。埏埴以为器,当其无,有器之用。凿户牖以为室,当其无,有室之用。故有之以为利,无之以为用。" },
|
||
{ h: "第十二章", t: "五色令人目盲,五音令人耳聋,五味令人口爽,驰骋畋猎令人心发狂,难得之货令人行妨。是以圣人为腹不为目,故去彼取此。" },
|
||
{ h: "第十三章", t: "宠辱若惊,贵大患若身。何谓宠辱若惊?宠为下,得之若惊,失之若惊,是谓宠辱若惊。何谓贵大患若身?吾所以有大患者,为吾有身,及吾无身,吾有何患?故贵以身为天下,若可寄天下;爱以身为天下,若可托天下。" },
|
||
{ h: "第十四章", t: "视之不见名曰夷,听之不闻名曰希,搏之不得名曰微。此三者,不可致诘,故混而为一。其上不皦,其下不昧。绳绳不可名,复归于无物。是谓无状之状,无物之象,是谓惚恍。迎之不见其首,随之不见其后。执古之道,以御今之有。能知古始,是谓道纪。" },
|
||
{ h: "第十五章", t: "古之善为士者,微妙玄通,深不可识。夫唯不可识,故强为之容。豫焉若冬涉川,犹兮若畏四邻,俨兮其若容,涣兮若冰之将释,敦兮其若朴,旷兮其若谷,混兮其若浊。孰能浊以静之徐清?孰能安以久动之徐生?保此道者不欲盈,夫唯不盈,故能蔽不新成。" },
|
||
{ h: "第十六章", t: "致虚极,守静笃。万物并作,吾以观复。夫物芸芸,各复归其根。归根曰静,是谓复命。复命曰常,知常曰明。不知常,妄作凶。知常容,容乃公,公乃全,全乃天,天乃道,道乃久,没身不殆。" },
|
||
{ h: "第十七章", t: "太上,下知有之,其次,亲而誉之,其次,畏之,其次,侮之。信不足焉,有不信焉。悠兮其贵言,功成事遂,百姓皆谓:我自然。" },
|
||
{ h: "第十八章", t: "大道废,有仁义;智慧出,有大伪;六亲不和,有孝慈;国家昏乱,有忠臣。" },
|
||
{ h: "第十九章", t: "绝圣弃智,民利百倍;绝仁弃义,民复孝慈;绝巧弃利,盗贼无有。此三者以为文不足,故令有所属﹕见素抱朴,少私寡欲。" },
|
||
{ h: "第二十章", t: "绝学无忧,唯之与阿,相去几何?善之与恶,相去若何?人之所畏,不可不畏。荒兮其未央哉﹗众人熙熙,如享太牢,如春登台。我独泊兮其未兆,如婴儿之未孩;儽儽兮若无所归。众人皆有余,而我独若遗。我愚人之心也哉﹗沌沌兮,俗人昭昭,我独昏昏。俗人察察,我独闷闷。澹兮其若海,飂兮若无止。众人皆有以,而我独顽似鄙。我独异于人,而贵食母。" },
|
||
{ h: "第二十一章", t: "孔德之容,惟道是从。道之为物,惟恍惟惚。惚兮恍兮,其中有象;恍兮惚兮,其中有物。窈兮冥兮,其中有精;其精甚真,其中有信。自今及古,其名不去,以阅众甫。吾何以知众甫之状哉?以此。" },
|
||
{ h: "第二十二章", t: "曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见故明,不自是故彰,不自伐故有功,不自矜故长。夫唯不争,故天下莫能与之争。古之所谓曲则全者,岂虚言哉!诚全而归之。" },
|
||
{ h: "第二十三章", t: "希言自然。故飘风不终朝,骤雨不终日。孰为此者?天地。天地尚不能久,而况于人乎?故从事于道者,道者同于道,德者同于德,失者同于失。同于道者,道亦乐得之;同于德者,德亦乐得之;同于失者,失亦乐得之。信不足焉,有不信焉。" },
|
||
{ h: "第二十四章", t: "企者不立,跨者不行,自见者不明,自是者不彰,自伐者无功,自矜者不长。其在道也,曰余食赘行。物或恶之,故有道者不处。" },
|
||
{ h: "第二十五章", t: "有物混成,先天地生。寂兮寥兮,独立而不改,周行而不殆,可以为天下母。吾不知其名,字之曰道,强为之名曰大。大曰逝,逝曰远,远曰反。故道大,天大,地大,王亦大。域中有四大,而王居其一焉。人法地,地法天,天法道,道法自然。" },
|
||
{ h: "第二十六章", t: "重为轻根,静为躁君。是以圣人终日行不离辎重。虽有荣观,燕处超然。奈何万乘之主,而以身轻天下?轻则失本,躁则失君。" },
|
||
{ h: "第二十七章", t: "善行无辙迹,善言无瑕讁;善数不用筹策;善闭无关楗而不可开,善结无绳约而不可解。是以圣人常善救人,故无弃人;常善救物,故无弃物,是谓袭明。故善人者,不善人之师;不善人者,善人之资。不贵其师,不爱其资,虽智大迷,是谓要妙。" },
|
||
{ h: "第二十八章", t: "知其雄,守其雌,为天下谿。为天下谿,常德不离,复归于婴儿。知其白,守其黑,为天下式。为天下式,常德不忒,复归于无极。知其荣,守其辱,为天下谷,常德乃足,复归于朴。朴散则为器,圣人用之,则为官长,故大制不割。" },
|
||
{ h: "第二十九章", t: "将欲取天下而为之,吾见其不得已。天下神器,不可为也,为者败之,执者失之。故物或行或随,或歔或吹。或强或羸,或挫或隳。是以圣人去甚,去奢,去泰。" },
|
||
{ h: "第三十章", t: "以道佐人主者,不以兵强天下。其事好还。师之所处,荆棘生焉。大军之后,必有凶年。善有果而已,不敢以取强。果而勿矜,果而勿伐,果而勿骄。果而不得已,果而勿强。物壮则老,是谓不道,不道早已。" },
|
||
{ h: "第三十一章", t: "夫佳兵者,不祥之器,物或恶之,故有道者不处。君子居则贵左,用兵则贵右。兵者不祥之器,非君子之器,不得已而用之,恬淡为上。胜而不美,而美之者,是乐杀人。夫乐杀人者,则不可以得志于天下矣。吉事尚左,凶事尚右。偏将军居左,上将军居右,言以丧礼处之。杀人之众,以哀悲泣之,战胜,以丧礼处之。" },
|
||
{ h: "第三十二章", t: "道常无名,朴虽小,天下莫能臣也。侯王若能守之,万物将自宾。天地相合,以降甘露,民莫之令而自均。始制有名,名亦既有,夫亦将知止,知止所以不殆。譬道之在天下,犹川谷之于江海。" },
|
||
{ h: "第三十三章", t: "知人者智,自知者明。胜人者有力,自胜者强。知足者富。强行者有志。不失其所者久。死而不亡者寿。" },
|
||
{ h: "第三十四章", t: "大道泛兮,其可左右。万物恃之而生而不辞,功成不名有。衣养万物而不为主,常无欲,可名于小;万物归焉而不为主,可名为大。以其终不自为大,故能成其大。" },
|
||
{ h: "第三十五章", t: "执大象,天下往。往而不害,安平太。乐与饵,过客止。道之出口,淡乎其无味,视之不足见,听之不足闻,用之不足既。" },
|
||
{ h: "第三十六章", t: "将欲歙之,必固张之;将欲弱之,必固强之;将欲废之,必固兴之;将欲夺之,必固与之。是谓微明。柔弱胜刚强。鱼不可脱于渊,国之利器不可以示人。" },
|
||
{ h: "第三十七章", t: "道常无为而无不为。侯王若能守之,万物将自化。化而欲作,吾将镇之以无名之朴。无名之朴,夫亦将无欲。不欲以静,天下将自定。# 老子《道德经》下篇" },
|
||
{ h: "第三十八章", t: "上德不德,是以有德;下德不失德,是以无德。上德无为而无以为,下德为之而有以为。上仁为之而无以为,上义为之而有以为。上礼为之而莫之应,则攘臂而扔之。故失道而后德,失德而后仁,失仁而后义,失义而后礼。夫礼者,忠信之薄,而乱之首。前识者,道之华,而愚之始。是以大丈夫处其厚,不居其薄;处其实,不居其华。故去彼取此。" },
|
||
{ h: "第三十九章", t: "昔之得一者,天得一以清,地得一以宁,神得一以灵,谷得一以盈,万物得一以生,侯王得一以为天下贞。其致之,天无以清将恐裂,地无以宁将恐发,神无以灵将恐歇,谷无以盈将恐竭,万物无以生将恐灭,侯王无以贵高将恐蹶。故贵以贱为本,高以下为基。是以侯王自称孤﹑寡﹑不谷。此非以贱为本邪?非乎?故致数舆无舆,不欲琭琭如玉,珞珞如石。" },
|
||
{ h: "第四十章", t: "反者道之动,弱者道之用。天下万物生于有,有生于无。" },
|
||
{ h: "第四十一章", t: "上士闻道,勤而行之;中士闻道,若存若亡;下士闻道,大笑之。不笑不足以为道。故建言有之﹕明道若昧,进道若退,夷道若颣,上德若谷,大白若辱,广德若不足,建德若偷,质真若渝,大方无隅,大器晚成,大音希声,大象无形,道隐无名。夫唯道,善贷且成。" },
|
||
{ h: "第四十二章", t: "道生一,一生二,二生三,三生万物。万物负阴而抱阳,冲气以为和。人之所恶,唯孤﹑寡﹑不谷,而王公以为称。故物或损之而益,或益之而损。人之所教,我亦教之。强梁者不得其死,吾将以为教父。" },
|
||
{ h: "第四十三章", t: "天下之至柔,驰骋天下之至坚。无有入无间,吾是以知无为之有益。不言之教,无为之益,天下希及之。" },
|
||
{ h: "第四十四章", t: "名与身孰亲?身与货孰多?得与亡孰病?是故甚爱必大费,多藏必厚亡,知足不辱,知止不殆,可以长久。" },
|
||
{ h: "第四十五章", t: "大成若缺,其用不弊。大盈若冲,其用不穷。大直若屈,其用不居。大巧若拙,其用不辍。大辩若讷,其用不差。躁胜寒,静胜热。清静为天下正。" },
|
||
{ h: "第四十六章", t: "天下有道,却走马以粪。天下无道,戎马生于郊。祸莫大于不知足;咎莫大于欲得。故知足之足,常足矣。" },
|
||
{ h: "第四十七章", t: "不出户,知天下;不闚牖,见天道。其出弥远,其知弥少。是以圣人不行而知,不见而名,不为而成。" },
|
||
{ h: "第四十八章", t: "为学日益,为道日损。损之又损,以至于无为。无为而无不为。取天下常以无事,及其有事,不足以取天下。" },
|
||
{ h: "第四十九章", t: "圣人无常心,以百姓心为心。善者,吾善之;不善者,吾亦善之,德善。信者,吾信之;不信者,吾亦信之,德信。圣人在天下歙歙,为天下浑其心,百姓皆注其耳目, 圣人皆孩之。" },
|
||
{ h: "第五十章", t: "出生入死。生之徒,十有三;死之徒,十有三;人之生,动之死地,亦十有三。夫何故?以其生生之厚。盖闻善摄生者,陆行不遇兕虎,入军不被甲兵;兕无所投其角,虎无所措其爪,兵无所容其刃。夫何故?以其无死地。" },
|
||
{ h: "第五十一章", t: "道生之,德畜之,物形之,势成之。是以万物莫不尊道而贵德。道之尊,德之贵,夫莫之命而常自然。故道生之,德畜之。长之育之,亭之毒之,盖之覆之。生而不有,为而不恃,长而不宰。是谓玄德。" },
|
||
{ h: "第五十二章", t: "天下有始,以为天下母。既得其母,以知其子,既知其子,复守其母,没身不殆。塞其兑,闭其门,终身不勤。开其兑,济其事,终身不救。见小曰明,守柔曰强。用其光,复归其明,无遗身殃,是为习常。" },
|
||
{ h: "第五十三章", t: "使我介然有知,行于大道,唯施是畏。大道甚夷,而民好径。朝甚除,田甚芜,仓甚虚;服文彩,带利剑,厌饮食,财货有余;是为盗夸。非道也哉!" },
|
||
{ h: "第五十四章", t: "善建者不拔,善抱者不脱,子孙以祭祀不辍。修之于身,其德乃真;修之于家,其德乃余;修之于乡,其德乃长;修之于国,其德乃丰;修之于天下,其德乃普。故以身观身,以家观家,以乡观乡,以国观国,以天下观天下。吾何以知天下然哉?以此。" },
|
||
{ h: "第五十五章", t: "含德之厚,比于赤子。蜂虿虺蛇不螫,猛兽不据,攫鸟不搏。骨弱筋柔而握固。未知牝牡之合而全作,精之至也。终日号而不嗄,和之至也。知和曰常,知常曰明。益生曰祥。心使气曰强。物壮则老,谓之不道,不道早已。" },
|
||
{ h: "第五十六章", t: "知者不言,言者不知。塞其兑,闭其门,挫其锐,解其分,和其光,同其尘,是谓玄同。故不可得而亲,不可得而疏;不可得而利,不可得而害;不可得而贵,不可得而贱。故为天下贵。" },
|
||
{ h: "第五十七章", t: "以正治国,以奇用兵,以无事取天下。吾何以知其然哉?以此。天下多忌讳,而民弥贫;民多利器,国家滋昬;人多伎巧,奇物滋起;法令滋彰,盗贼多有。故圣人云﹕「我无为而民自化,我好静而民自正,我无事而民自富,我无欲而民自朴。」" },
|
||
{ h: "第五十八章", t: "其政闷闷,其民淳淳;其政察察,其民缺缺。祸兮福之所倚,福兮祸之所伏。孰知其极?其无正。正复为奇,善复为妖。人之迷,其日固久。是以圣人方而不割,廉而不刿,直而不肆,光而不燿。" },
|
||
{ h: "第五十九章", t: "治人事天,莫若啬。夫唯啬,是谓早服;早服谓之重积德;重积德则无不克,无不克则莫知其极;莫知其极,可以有国;有国之母,可以长久;是谓深根固柢,长生久视之道。" },
|
||
{ h: "第六十章", t: "治大国,若烹小鲜。以道莅天下,其鬼不神;非其鬼不神,其神不伤人;非其神不伤人,圣人亦不伤人。夫两不相伤,故德交归焉。" },
|
||
{ h: "第六十一章", t: "大国者下流,天下之交。天下之牝,牝常以静胜牡,以静为下。故大国以下小国,则取小国;小国以下大国,则取大国。故或下以取,或下而取。大国不过欲兼畜人,小国不过欲入事人。夫两者各得其所欲,大者宜为下。" },
|
||
{ h: "第六十二章", t: "道者万物之奥。善人之宝,不善人之所保。美言可以市,尊行可以加人。人之不善,何弃之有?故立天子,置三公,虽有拱璧以先驷马,不如坐进此道。古之所以贵此道者何?不曰以求得,有罪以免邪?故为天下贵。" },
|
||
{ h: "第六十三章", t: "为无为,事无事,味无味。大小多少,报怨以德。图难于其易,为大于其细;天下难事必作于易,天下大事必作于细。是以圣人终不为大,故能成其大。夫轻诺必寡信,多易必多难。是以圣人犹难之,故终无难矣" },
|
||
{ h: "第六十四章", t: "其安易持,其未兆易谋。其脆易泮,其微易散。为之于未有,治之于未乱。合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。为者败之,执者失之。是以圣人无为故无败,无执故无失。民之从事,常于几成而败之。慎终如始,则无败事。是以圣人欲不欲,不贵难得之货;学不学,复众人之所过。以辅万物之自然,而不敢为。" },
|
||
{ h: "第六十五章", t: "古之善为道者,非以明民,将以愚之。民之难治,以其智多。故以智治国,国之贼,不以智治国,国之福。知此两者亦稽式。常知稽式,是谓玄德。玄德深矣,远矣,与物反矣,然后乃至大顺。" },
|
||
{ h: "第六十六章", t: "江海所以能为百谷王者,以其善下之,故能为百谷王。是以欲上民,必以言下之。欲先民,必以身后之。是以圣人处上而民不重,处前而民不害。是以天下乐推而不厌,以其不争,故天下莫能与之争。" },
|
||
{ h: "第六十七章", t: "天下皆谓我道大,似不肖。夫唯大,故似不肖。若肖,久矣其细也夫!我有三宝,持而保之。一曰慈,二曰俭,三曰不敢为天下先。 慈故能勇,俭故能广,不敢为天下先,故能成器长。今舍慈且勇,舍俭且广,舍后且先,死矣! 夫慈以战则胜,以守则固。天将救之,以慈卫之。" },
|
||
{ h: "第六十八章", t: "善为士者不武,善战者不怒,善胜敌者不与,善用人者为之下,是谓不争之德,是谓用人之力,是谓配天古之极。" },
|
||
{ h: "第六十九章", t: "用兵有言﹕「吾不敢为主而为客,不敢进寸而退尺。」是谓行无行,攘无臂,扔无敌,执无兵。 祸莫大于轻敌,轻敌几丧吾宝。故抗兵相加,哀者胜矣。" },
|
||
{ h: "第七十章", t: "吾言甚易知,甚易行。天下莫能知,莫能行。言有宗,事有君。夫唯无知,是以不我知。知我者希,则我者贵。是以圣人被褐怀玉。" },
|
||
{ h: "第七十一章", t: "知不知上,不知知病。夫唯病病,是以不病。圣人不病,以其病病,是以不病。" },
|
||
{ h: "第七十二章", t: "民不畏威,则大威至。无狎其所居,无厌其所生。夫唯不厌,是以不厌。是以圣人自知不自见,自爱不自贵;故去彼取此。" },
|
||
{ h: "第七十三章", t: "勇于敢则杀,勇于不敢则活。此两者,或利或害。天之所恶,孰知其故?是以圣人犹难之。天之道,不争而善胜,不言而善应,不召而自来,𦈎然而善谋。天网恢恢,疏而不失。" },
|
||
{ h: "第七十四章", t: "民不畏死,奈何以死惧之?若使民常畏死,而为奇者,吾得执而杀之,孰敢?常有司杀者杀,夫代司杀者杀,是谓代大匠斲。 夫代大匠斲者,希有不伤其手矣。" },
|
||
{ h: "第七十五章", t: "民之饥,以其上食税之多,是以饥。民之难治,以其上之有为,是以难治。民之轻死,以其上求生之厚,是以轻死。夫唯无以生为者,是贤于贵生。" },
|
||
{ h: "第七十六章", t: "人之生也柔弱,其死也坚强。 万物草木之生也柔脆,其死也枯槁。 故坚强者死之徒,柔弱者生之徒。是以兵强则不胜,木强则兵。强大处下,柔弱处上。" },
|
||
{ h: "第七十七章", t: "天之道,其犹张弓与?高者抑之,下者举之;有余者损之,不足者补之。天之道,损有余而补不足。人之道则不然,损不足以奉有余。 孰能有余以奉天下?唯有道者。 是以圣人为而不恃,功成而不处,其不欲见贤。" },
|
||
{ h: "第七十八章", t: "天下莫柔弱于水,而攻坚强者,莫之能胜,其无以易之。弱之胜强,柔之胜刚,天下莫不知莫能行。是以圣人云﹕「受国之垢,是谓社稷主;受国不祥,是为天下王。」正言若反。" },
|
||
{ h: "第七十九章", t: "和大怨,必有余怨,安可以为善?是以圣人执左契,而不责于人。有德司契,无德司彻。天道无亲,常与善人。" },
|
||
{ h: "第八十章", t: "小国寡民,使有什伯之器而不用,使民重死而不远徙。虽有舟舆,无所乘之;虽有甲兵,无所陈之。使人复结绳而用之,甘其食,美其服,安其居,乐其俗。邻国相望,鸡犬之声相闻,民至老死,不相往来。" },
|
||
{ h: "第八十一章", t: "信言不美,美言不信。善者不辩,辩者不善。 知者不博,博者不知。 圣人不积,既以为人己愈有,既以与人己愈多。天之道,利而不害;圣人之道,为而不争。" },
|
||
],
|
||
},
|
||
{
|
||
id: "qingjing",
|
||
name: "清静经",
|
||
title: "太上老君说常清静经",
|
||
seal: "清静经",
|
||
cat: "道家",
|
||
translator: "正统道藏本",
|
||
source: "维基文库 · 正统道藏",
|
||
sourceUrl: "https://zh.wikisource.org/wiki/太上老君說常清靜經",
|
||
pageSize: 6, // 一屏读完
|
||
segments: [
|
||
{ t: "老君曰:大道无形,生育天地;大道无情,运行日月;大道无名,长养万物;吾不知其名,强名曰道。夫道者:有清有浊,有动有静;天清地浊,天动地静;男清女浊,男动女静。降本流末,而生万物。清者浊之源,动者静之基。人能常清静,天地悉皆归。" },
|
||
{ t: "夫人神好清,而心扰之;人心好静,而欲牵之。常能遣其欲而心自静,澄其心而神自清。自然六欲不生,三毒消灭。所以不能者,为心未澄,欲未遣也。能遣之者,内观于心,心无其心;外观于形,形无其形;远观于物,物无其物。三者既悟,唯见于空。观空亦空,空无所空;所空既无,无无亦无;无无既无,湛然常寂;寂无所寂,欲岂能生?欲既不生,即是真静。真常应物,真常得性;常应常静,常清静矣。如此清静,渐入真道;既入真道,名谓得道,虽名得道,实无所得;为化众生,名谓得道;能悟之者,可传圣道。" },
|
||
{ t: "老君曰:上士无争,下士好争;上德不德,下德执德;执著之者,不名道德。众生所以不得真道者,为有妄心。既有妄心,即惊其神;既惊其神,即著万物;既著万物,即生贪求;既生贪求,即是烦恼;烦恼妄想,忧苦身心;便遭浊辱,流浪生死,常沉苦海,永失真道。真常之道,悟者自得。得悟道者,常清静矣。" },
|
||
{ t: "仙人葛玄曰:吾得真道,曾诵此经万遍。此经是天人所习,不传下士。吾昔受之于东华帝君。东华帝君受之于金阙帝君,金阙帝君受之于西王母。西王母皆口口相传,不记文字。吾今于世书而录之。上士悟之,升为天官。中士修之,南宫列仙。下士得之,在世长年,游行三界,升入金门。" },
|
||
{ t: "左玄真人曰:学道之士,持诵此经,即得十天善神拥护其人。然后玉符宝神,金液链形,形神俱妙,与道合真。" },
|
||
{ t: "正一真人曰:人家有此经,悟解之者,灾障不干,众圣护门,神升上界,朝拜高尊。功满德就,相感帝君。诵持不退,身腾紫云。" },
|
||
],
|
||
},
|
||
{
|
||
id: "ganying",
|
||
name: "感应篇",
|
||
title: "太上感应篇",
|
||
seal: "感应篇",
|
||
cat: "道家",
|
||
translator: "通行本",
|
||
source: "维基文库",
|
||
sourceUrl: "https://zh.wikisource.org/wiki/太上感應篇",
|
||
pageSize: 4, // 每页 4 段
|
||
segments: [
|
||
{ t: "太上曰:「祸福无门,惟人自召;善恶之报,如影随形。」" },
|
||
{ t: "是以天地有司过之神,依人所犯轻重,以夺人算。算减则贫耗,多逢忧患;人皆恶之,刑祸随之,吉庆避之,恶星灾之;算尽则死。又有三台、北斗神君,在人头上,录人罪恶,夺其纪算。又有三尸神,在人身中,每到庚申日,辄上诣天曹,言人罪过。月晦之日,灶神亦然。凡人有过,大则夺纪,小则夺算。其过大小,有数百事,欲求长生者,先须避之。" },
|
||
{ t: "是道则进,非道则退。不履邪径,不欺暗室;积德累功,慈心于物;忠孝友悌,正己化人;矜孤恤寡,敬老怀幼;昆虫草木,犹不可伤。宜悯人之凶,乐人之善;济人之急,救人之危。见人之得,如己之得;见人之失,如己之失。不彰人短,不炫己长;遏恶扬善,推多取少。受辱不怨,受宠若惊。施恩不求报,与人不追悔。" },
|
||
{ t: "所谓善人,人皆敬之,天道佑之,福禄随之,众邪远之,神灵卫之;所作必成,神仙可冀。欲求天仙者,当立一千三百善;欲求地仙者,当立三百善。" },
|
||
{ t: "苟或非义而动,背理而行。以恶为能,忍作残害;阴贼良善,暗侮君亲;慢其先生,叛其所事;诳诸无识,谤诸同学;虚诬诈伪,攻讦宗亲;刚强不仁,狠戾自用;是非不当,向背乖宜;虐下取功,谄上希旨;受恩不感,念怨不休;轻蔑天民,扰乱国政;赏及非义,刑及无辜;杀人取财,倾人取位;诛降戮服,贬正排贤;凌孤逼寡,弃法受赂;以直为曲,以曲为直;入轻为重,见杀加怒;知过不改,知善不为;自罪引他,壅塞方术;讪谤圣贤,侵凌道德。" },
|
||
{ t: "射飞逐走,发蛰惊栖;填穴覆巢,伤胎破卵;愿人有失,毁人成功;危人自安,减人自益;以恶易好,以私废公,窃人之能,蔽人之善;形人之丑,讦人之私;耗人货财,离人骨肉;侵人所爱,助人为非;逞志作威,辱人求胜;败人苗稼,破人婚姻;苟富而骄,苟免无耻;认恩推过,嫁祸卖恶;沽买虚誉,包贮险心;挫人所长,护己所短;乘威迫胁,纵暴杀伤;无故剪裁,非礼烹宰;散弃五谷,劳扰众生;破人之家,取其财宝;决水放火,以害民居;紊乱规模,以败人功;损人器物,以穷人用。" },
|
||
{ t: "见他荣贵,愿他流贬;见他富有,愿他破散;见他色美,起心私之;负他货财,愿他身死;干求不遂,便生咒恨;见他失便,便说他过;见他体相不具而笑之,见他才能可称而抑之。" },
|
||
{ t: "埋蛊厌人,用药杀树;恚怒师傅,抵触父兄;强取强求,好侵好夺;掳掠致富,巧诈求迁;赏罚不平,逸乐过节;苛虐其下,恐吓于他;怨天尤人,呵风骂雨;鬪合争讼,妄逐朋党;用妻妾语,违父母训;得新忘故,口是心非;贪冒于财,欺罔其上;造作恶语,谗毁平人;毁人称直,骂神称正;弃顺效逆,背亲向疏;指天地以证鄙怀,引神明而鉴猥事。" },
|
||
{ t: "施与后悔,假借不还;分外营求,力上施设;淫欲过度,心毒貌慈;秽食𫗪人,左道惑众;短尺狭度,轻秤小升;以伪杂真,采取奸利;压良为贱,谩蓦愚人;贪婪无厌,咒诅求直。" },
|
||
{ t: "嗜酒悖乱,骨肉忿争;男不忠良,女不柔顺;不和其室,不敬其夫;每好矜夸,常行妒忌;无行于妻子,失礼于舅姑;轻慢先灵,违逆上命;作为无益,怀挟外心;自咒咒他,偏憎偏爱;越井越灶,跳食跳人;损子堕胎,行多隐僻;晦腊歌舞,朔旦号怒;对北涕唾及溺,对灶吟咏及哭;又以灶火烧香,秽柴作食;夜起裸露,八节行刑;唾流星,指虹霓;辄指三光,久视日月;春月燎猎,对北恶骂,无故杀龟打蛇……如是等罪,司命随其轻重,夺其纪算。算尽则死;死有余责,乃殃及子孙。" },
|
||
{ t: "又诸横取人财者,乃计其妻子、家口以当之,渐至死丧。若不死丧,则有水火盗贼、遗亡器物、疾病口舌诸事,以当妄取之值。又枉杀人者,是易刀兵而相杀也。取非义之财者,譬如漏脯救饥,鸩酒止渴;非不暂饱,死亦及之。" },
|
||
{ t: "夫心起于善,善虽未为,而吉神已随之;或心起于恶,恶虽未为,而凶神已随之。其有曾行恶事,后自改悔,诸恶莫作,众善奉行,久久必获吉庆;所谓转祸为福也。故吉人语善、视善、行善,一日有三善,三年天必降之福。凶人语恶、视恶、行恶,一日有三恶,三年天必降之祸。胡不勉而行之?" },
|
||
],
|
||
},
|
||
];
|
||
|
||
// ===== 可选音色(模型 qwen3-tts-flash) =====
|
||
const VOICE_GROUPS = [
|
||
{
|
||
label: "诵读音色",
|
||
voices: [
|
||
["Seren", "Seren 塞伦 · 女声"],
|
||
["Ethan", "Ethan 伊森 · 男声"],
|
||
],
|
||
},
|
||
];
|
||
|
||
const sutraEl = document.getElementById("sutra");
|
||
const sutraSelect = document.getElementById("sutra-select");
|
||
const voiceSelect = document.getElementById("voice-select");
|
||
const btnPlay = document.getElementById("btn-play");
|
||
const statusEl = document.getElementById("status");
|
||
const progressEl = document.getElementById("progress");
|
||
const audioEl = document.getElementById("audio");
|
||
const btnLayout = document.getElementById("btn-layout");
|
||
const pagerEl = document.getElementById("pager");
|
||
const playerCard = document.getElementById("player-card");
|
||
const btnExpand = document.getElementById("btn-expand");
|
||
const btnPrev = document.getElementById("btn-prev");
|
||
const btnNext = document.getElementById("btn-next");
|
||
const btnLoop = document.getElementById("btn-loop");
|
||
const btnBgm = document.getElementById("btn-bgm");
|
||
const timerSelect = document.getElementById("timer-select");
|
||
const rateSelect = document.getElementById("rate-select");
|
||
const btnNight = document.getElementById("btn-night");
|
||
const sleepRemainEl = document.getElementById("sleep-remaining");
|
||
const btnJumpNow = document.getElementById("btn-jump-now");
|
||
const pageInfo = document.getElementById("page-info");
|
||
|
||
/** 音频缓存:key = `${经id}|${音色}|${段序号}`,value = Blob */
|
||
const audioCache = new Map();
|
||
let playToken = 0; // 播放会话令牌,防止旧异步任务串扰
|
||
let playing = false;
|
||
let inflight = null; // 进行中的 fetch AbortController
|
||
let loopMode = false; // 循环诵读开关:播完全文回到开头继续
|
||
let bgmOn = false; // 背景音乐开关
|
||
let sleepEndAt = 0; // 定时停止的截止时间戳(毫秒),0 = 未开启
|
||
let sleepTimer = null; // 定时器句柄(前台兜底;后台由 onTimeUpdate 判定截止时间)
|
||
let loopAutoByTimer = false; // 循环是否为设定定时时自动打开的(是则定时结束一并关回)
|
||
|
||
// ===== 用户偏好记忆(localStorage):经、音色、字号、循环、配乐、语速、夜间 =====
|
||
const PREFS_KEY = "fj.prefs.v1";
|
||
const prefs = (() => {
|
||
try {
|
||
return JSON.parse(localStorage.getItem(PREFS_KEY)) || {};
|
||
} catch {
|
||
return {};
|
||
}
|
||
})();
|
||
/** 写一条偏好并落盘;localStorage 不可用时静默降级为仅内存。 */
|
||
function savePref(key, value) {
|
||
prefs[key] = value;
|
||
try {
|
||
localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));
|
||
} catch { /* 隐私模式等场景忽略 */ }
|
||
}
|
||
let segTimes = []; // 合并音频中各段的起始秒
|
||
let currentSeg = -1; // 正在朗读的段号
|
||
let lastUserScroll = 0; // 用户最近一次手动滚动经文的时间戳(用于暂缓跟读强拽)
|
||
let cursorRaf = null; // 逐字光标 rAF 句柄
|
||
|
||
// ===== 背景音乐:Web Audio 实时合成颂钵泛音 drone(零资源文件、无缝循环) =====
|
||
let bgmCtx = null;
|
||
let bgmGain = null;
|
||
let bgmNodes = [];
|
||
let bgmBellTimer = null;
|
||
let bgmDrumTimer = null;
|
||
let bgmChimeTimer = null;
|
||
let bgmGlideTimer = null;
|
||
let bgmOscs = []; // {osc, ratio},供基频滑变统一改频率
|
||
const BGM_ROOTS = [55, 65.41, 73.42, 82.41, 98]; // A1 C2 D2 E2 G2,低音区垫底
|
||
let bgmRoot = 73.42;
|
||
|
||
/** 基频慢移:只滑向相邻根音、16 秒缓慢过渡,变化若有似无,避免警报式的滑音。 */
|
||
function bgmGlide() {
|
||
const idx = BGM_ROOTS.indexOf(bgmRoot);
|
||
const neighbors = [idx - 1, idx + 1].filter((i) => i >= 0 && i < BGM_ROOTS.length);
|
||
bgmRoot = BGM_ROOTS[neighbors[Math.floor(Math.random() * neighbors.length)]];
|
||
const t = bgmCtx.currentTime;
|
||
for (const { osc, ratio } of bgmOscs) {
|
||
osc.frequency.cancelScheduledValues(t);
|
||
osc.frequency.setValueAtTime(osc.frequency.value, t);
|
||
osc.frequency.exponentialRampToValueAtTime(bgmRoot * ratio, t + 16);
|
||
}
|
||
bgmGlideTimer = setTimeout(() => { if (bgmGain) bgmGlide(); }, 50000 + Math.random() * 40000);
|
||
}
|
||
|
||
/** 给打击声部接随机方位的输出链,返回挂到主增益下的 GainNode。 */
|
||
function bgmStrikeOut(panRange) {
|
||
const out = bgmCtx.createGain();
|
||
if (bgmCtx.createStereoPanner) {
|
||
const pan = bgmCtx.createStereoPanner();
|
||
pan.pan.value = (Math.random() - 0.5) * 2 * panRange;
|
||
out.connect(pan);
|
||
pan.connect(bgmGain);
|
||
} else {
|
||
out.connect(bgmGain);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** 晨钟一声:大钟的非谐泛音簇,毫秒级起音,泛音越高衰减越快(长达十余秒)。 */
|
||
function bgmBell() {
|
||
const now = bgmCtx.currentTime;
|
||
const root = [98, 110, 123.47][Math.floor(Math.random() * 3)]; // G2 / A2 / B2 附近的大钟
|
||
const bellGain = bgmStrikeOut(0.8);
|
||
bellGain.gain.setValueAtTime(0, now);
|
||
bellGain.gain.linearRampToValueAtTime(0.5, now + 0.02);
|
||
// 大钟泛音列:哼音 0.5、基音 1、小三度 1.19、五度 1.5、名义音 2、上泛音 2.74
|
||
// [频率比, 振幅, 衰减秒数] —— 低频泛音余韵更长
|
||
[[0.5, 0.5, 15], [1, 1, 13], [1.19, 0.3, 9], [1.5, 0.24, 7], [2.0, 0.18, 5], [2.74, 0.1, 3.5]].forEach(([ratio, amp, dur]) => {
|
||
const o = bgmCtx.createOscillator();
|
||
o.type = "sine";
|
||
o.frequency.value = root * ratio;
|
||
o.detune.value = (Math.random() - 0.5) * 5;
|
||
const og = bgmCtx.createGain();
|
||
og.gain.setValueAtTime(amp, now);
|
||
og.gain.exponentialRampToValueAtTime(0.0001, now + dur);
|
||
o.connect(og);
|
||
og.connect(bellGain);
|
||
o.start(now);
|
||
o.stop(now + dur + 0.5);
|
||
bgmNodes.push(o);
|
||
});
|
||
}
|
||
|
||
/** 磬声一声:高音引磬,快起音 + 约 6 秒衰减的小泛音簇,稀疏点缀。 */
|
||
function bgmChime() {
|
||
const now = bgmCtx.currentTime;
|
||
const freq = [392, 523.25, 659.25, 783.99][Math.floor(Math.random() * 4)]; // 五声音阶附近
|
||
const chimeGain = bgmStrikeOut(0.6);
|
||
chimeGain.gain.setValueAtTime(0, now);
|
||
chimeGain.gain.linearRampToValueAtTime(0.35, now + 0.03);
|
||
chimeGain.gain.exponentialRampToValueAtTime(0.0001, now + 6);
|
||
[[1, 1], [2.76, 0.28], [5.4, 0.08]].forEach(([ratio, amp]) => {
|
||
const o = bgmCtx.createOscillator();
|
||
o.type = "sine";
|
||
o.frequency.value = freq * ratio;
|
||
o.detune.value = (Math.random() - 0.5) * 6;
|
||
const og = bgmCtx.createGain();
|
||
og.gain.value = amp;
|
||
o.connect(og);
|
||
og.connect(chimeGain);
|
||
o.start(now);
|
||
o.stop(now + 6.5);
|
||
bgmNodes.push(o);
|
||
});
|
||
}
|
||
|
||
/** 暮鼓一记:低频膜鸣(72→44Hz 下滑)+ 短促噪声击边,钝而沉。 */
|
||
function bgmDrum() {
|
||
const now = bgmCtx.currentTime;
|
||
const drumGain = bgmStrikeOut(0.5);
|
||
drumGain.gain.setValueAtTime(0, now);
|
||
drumGain.gain.linearRampToValueAtTime(0.55, now + 0.012);
|
||
drumGain.gain.exponentialRampToValueAtTime(0.0001, now + 1.8);
|
||
|
||
// 鼓皮基音:起振约 72Hz,迅速下滑到 44Hz
|
||
const o = bgmCtx.createOscillator();
|
||
o.type = "sine";
|
||
o.frequency.setValueAtTime(72, now);
|
||
o.frequency.exponentialRampToValueAtTime(44, now + 0.3);
|
||
o.connect(drumGain);
|
||
o.start(now);
|
||
o.stop(now + 2);
|
||
bgmNodes.push(o);
|
||
|
||
// 击边噪声:0.08s 噪声过 260Hz 带通,模拟鼓槌触皮的一声"扑"
|
||
const len = Math.floor(bgmCtx.sampleRate * 0.08);
|
||
const buf = bgmCtx.createBuffer(1, len, bgmCtx.sampleRate);
|
||
const data = buf.getChannelData(0);
|
||
for (let i = 0; i < len; i += 1) data[i] = Math.random() * 2 - 1;
|
||
const noise = bgmCtx.createBufferSource();
|
||
noise.buffer = buf;
|
||
const bp = bgmCtx.createBiquadFilter();
|
||
bp.type = "bandpass";
|
||
bp.frequency.value = 260;
|
||
bp.Q.value = 0.8;
|
||
const ng = bgmCtx.createGain();
|
||
ng.gain.setValueAtTime(0.4, now);
|
||
ng.gain.exponentialRampToValueAtTime(0.0001, now + 0.15);
|
||
noise.connect(bp);
|
||
bp.connect(ng);
|
||
ng.connect(drumGain);
|
||
noise.start(now);
|
||
bgmNodes.push(noise);
|
||
}
|
||
|
||
/**
|
||
* 开始播放背景音乐:低频颂钵式泛音 drone 垫底(基频缓慢游走),
|
||
* 叠稀疏的晨钟(大钟非谐泛音簇、长余韵)、暮鼓(低频膜鸣 + 击边噪声)
|
||
* 与引磬(高音泛音点缀),三种寺院打击声部随机方位错开出现,
|
||
* 方位随机、音量压在人声之下;2.5 秒淡入。需在用户手势内调用。
|
||
*/
|
||
function startBgm() {
|
||
if (!bgmCtx) bgmCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||
if (bgmCtx.state === "suspended") bgmCtx.resume();
|
||
const now = bgmCtx.currentTime;
|
||
const g = bgmCtx.createGain();
|
||
g.gain.setValueAtTime(0, now);
|
||
// 低通只做轻微软化,保留高频空气感(太低会发闷)
|
||
const lp = bgmCtx.createBiquadFilter();
|
||
lp.type = "lowpass";
|
||
lp.frequency.value = 3200;
|
||
lp.Q.value = 0.4;
|
||
g.connect(lp);
|
||
lp.connect(bgmCtx.destination);
|
||
|
||
const base = bgmRoot; // 初始根音,之后由 bgmGlide 慢移
|
||
const partials = [[1, 0.45], [2.02, 0.2], [3.01, 0.13], [4.16, 0.08], [5.43, 0.05], [7.2, 0.025]];
|
||
const nodes = [];
|
||
bgmOscs = [];
|
||
partials.forEach(([ratio, amp], pi) => {
|
||
const og = bgmCtx.createGain();
|
||
og.gain.value = amp;
|
||
// 泛音左右宽展开 + 极慢声像漂移,空间缓慢呼吸
|
||
if (bgmCtx.createStereoPanner) {
|
||
const pan = bgmCtx.createStereoPanner();
|
||
const side = pi % 2 === 0 ? -1 : 1;
|
||
pan.pan.value = side * 0.6;
|
||
og.connect(pan);
|
||
pan.connect(g);
|
||
const plfo = bgmCtx.createOscillator();
|
||
plfo.frequency.value = 0.02 + Math.random() * 0.03;
|
||
const plg = bgmCtx.createGain();
|
||
plg.gain.value = 0.3;
|
||
plfo.connect(plg);
|
||
plg.connect(pan.pan);
|
||
plfo.start(now + pi * 0.5);
|
||
nodes.push(plfo);
|
||
} else {
|
||
og.connect(g);
|
||
}
|
||
// 双振荡器 ±3 音分失谐,产生颂钵的拍频颤动
|
||
for (const det of [-3, 3]) {
|
||
const osc = bgmCtx.createOscillator();
|
||
osc.type = "sine";
|
||
osc.frequency.value = base * ratio;
|
||
osc.detune.value = det + (Math.random() - 0.5) * 2;
|
||
osc.connect(og);
|
||
osc.start(now + Math.random() * 0.3);
|
||
nodes.push(osc);
|
||
bgmOscs.push({ osc, ratio });
|
||
}
|
||
// 每个泛音带独立慢速呼吸,避免死板
|
||
const lfo = bgmCtx.createOscillator();
|
||
lfo.frequency.value = 0.04 + Math.random() * 0.07;
|
||
const lg = bgmCtx.createGain();
|
||
lg.gain.value = amp * 0.4;
|
||
lfo.connect(lg);
|
||
lg.connect(og.gain);
|
||
lfo.start(now);
|
||
nodes.push(lfo);
|
||
});
|
||
g.gain.linearRampToValueAtTime(0.07, now + 2.5); // 淡入,drone 压低垫底
|
||
bgmGain = g;
|
||
bgmNodes = nodes;
|
||
|
||
// 晨钟:淡入后 4 秒先撞一声,之后每 30~55 秒一声
|
||
const scheduleBell = () => {
|
||
bgmBellTimer = setTimeout(() => {
|
||
if (!bgmGain) return;
|
||
bgmBell();
|
||
scheduleBell();
|
||
}, 30000 + Math.random() * 25000);
|
||
};
|
||
bgmBellTimer = setTimeout(() => {
|
||
if (!bgmGain) return;
|
||
bgmBell();
|
||
scheduleBell();
|
||
}, 4000);
|
||
|
||
// 暮鼓:每 45~80 秒一记,偶尔连击两记(间隔约 2.4s)
|
||
const scheduleDrum = () => {
|
||
bgmDrumTimer = setTimeout(() => {
|
||
if (!bgmGain) return;
|
||
bgmDrum();
|
||
if (Math.random() < 0.45) setTimeout(() => { if (bgmGain) bgmDrum(); }, 2400);
|
||
scheduleDrum();
|
||
}, 45000 + Math.random() * 35000);
|
||
};
|
||
scheduleDrum();
|
||
|
||
// 引磬:最稀疏,每 60~100 秒一声清越点缀
|
||
const scheduleChime = () => {
|
||
bgmChimeTimer = setTimeout(() => {
|
||
if (!bgmGain) return;
|
||
bgmChime();
|
||
scheduleChime();
|
||
}, 60000 + Math.random() * 40000);
|
||
};
|
||
scheduleChime();
|
||
|
||
// 首次基频滑变:约 50 秒后
|
||
bgmGlideTimer = setTimeout(() => { if (bgmGain) bgmGlide(); }, 50000);
|
||
}
|
||
|
||
/** 停止背景音乐:1.5 秒淡出后停掉振荡器、钟鼓调度器并断开节点。 */
|
||
function stopBgm() {
|
||
if (bgmBellTimer) {
|
||
clearTimeout(bgmBellTimer);
|
||
bgmBellTimer = null;
|
||
}
|
||
if (bgmDrumTimer) {
|
||
clearTimeout(bgmDrumTimer);
|
||
bgmDrumTimer = null;
|
||
}
|
||
if (bgmChimeTimer) {
|
||
clearTimeout(bgmChimeTimer);
|
||
bgmChimeTimer = null;
|
||
}
|
||
if (bgmGlideTimer) {
|
||
clearTimeout(bgmGlideTimer);
|
||
bgmGlideTimer = null;
|
||
}
|
||
if (!bgmCtx || !bgmGain) return;
|
||
const g = bgmGain;
|
||
const nodes = bgmNodes;
|
||
const now = bgmCtx.currentTime;
|
||
g.gain.cancelScheduledValues(now);
|
||
g.gain.setValueAtTime(g.gain.value, now);
|
||
g.gain.linearRampToValueAtTime(0, now + 1.5);
|
||
bgmGain = null;
|
||
bgmNodes = [];
|
||
setTimeout(() => {
|
||
nodes.forEach((n) => { try { n.stop(); } catch { /* 已停止 */ } });
|
||
g.disconnect();
|
||
}, 1700);
|
||
}
|
||
|
||
let sutra = null; // 当前经
|
||
let segments = []; // 当前经分段
|
||
let currentPage = 0;
|
||
let pageCount = 1;
|
||
let segEls = [];
|
||
let dots = [];
|
||
|
||
const currentVoice = () => voiceSelect.value;
|
||
const pageOf = (i) => Math.floor(i / sutra.pageSize);
|
||
|
||
// ===== 音色下拉 =====
|
||
VOICE_GROUPS.forEach((group) => {
|
||
const target = VOICE_GROUPS.length > 1 ? voiceSelect.appendChild(Object.assign(document.createElement("optgroup"), { label: group.label })) : voiceSelect;
|
||
group.voices.forEach(([value, label]) => {
|
||
const opt = document.createElement("option");
|
||
opt.value = value;
|
||
opt.textContent = label;
|
||
target.appendChild(opt);
|
||
});
|
||
});
|
||
voiceSelect.value = "Seren";
|
||
|
||
// ===== 经典下拉:按 cat 分组(佛经 / 道家) =====
|
||
{
|
||
const cats = [...new Set(SUTRAS.map((s) => s.cat || "其他"))];
|
||
cats.forEach((cat) => {
|
||
const group = sutraSelect.appendChild(Object.assign(document.createElement("optgroup"), { label: cat }));
|
||
SUTRAS.filter((s) => (s.cat || "其他") === cat).forEach((s) => {
|
||
const opt = document.createElement("option");
|
||
opt.value = s.id;
|
||
opt.textContent = s.name;
|
||
group.appendChild(opt);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ===== 状态 / 高亮 =====
|
||
let statusBase = ""; // 状态主干文字;定时开启时自动追加剩余倒计时
|
||
function setStatus(text, isError = false) {
|
||
statusBase = text;
|
||
statusEl.classList.toggle("error", isError);
|
||
renderStatus();
|
||
}
|
||
|
||
/** 渲染状态条:主干 + 定时剩余 mm:ss;移动端同步到播放旁的剩余胶囊。 */
|
||
function renderStatus() {
|
||
let remain = "";
|
||
if (sleepEndAt) {
|
||
const s = Math.max(0, Math.ceil((sleepEndAt - Date.now()) / 1000));
|
||
remain = `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
||
}
|
||
statusEl.textContent = statusBase + (remain ? ` · ${remain} 后停` : "");
|
||
sleepRemainEl.textContent = remain ? `⏱ ${remain}` : "";
|
||
sleepRemainEl.classList.toggle("show", !!sleepEndAt);
|
||
}
|
||
setInterval(renderStatus, 1000);
|
||
|
||
function setPlayingUI(on) {
|
||
playing = on;
|
||
btnPlay.classList.toggle("playing", on);
|
||
btnPlay.setAttribute("aria-label", on ? "停止" : "播放");
|
||
btnPlay.querySelector(".play-label").textContent = on ? "停 止" : "播 放";
|
||
if ("mediaSession" in navigator) navigator.mediaSession.playbackState = on ? "playing" : "none";
|
||
}
|
||
|
||
/** 更新锁屏/控制中心元数据:经名 + 音色 + 印章封面,并接管播放控制按键。 */
|
||
function updateMediaSession() {
|
||
if (!("mediaSession" in navigator) || !sutra) return;
|
||
navigator.mediaSession.metadata = new MediaMetadata({
|
||
title: `《${sutra.name}》诵读`,
|
||
artist: currentVoice(),
|
||
album: sutra.title,
|
||
artwork: [
|
||
{ src: "icon-180.png", sizes: "180x180", type: "image/png" },
|
||
{ src: "icon.svg", sizes: "any", type: "image/svg+xml" },
|
||
],
|
||
});
|
||
}
|
||
|
||
function highlight(index) {
|
||
segEls.forEach((el) => {
|
||
const i = Number(el.dataset.index);
|
||
el.classList.toggle("playing", i === index);
|
||
el.classList.toggle("done", i < index);
|
||
});
|
||
dots.forEach((d, i) => {
|
||
d.classList.toggle("now", i === index);
|
||
d.classList.toggle("done", index >= 0 && i < index);
|
||
});
|
||
// 跟读滚动:定位到光标所在行(而非整段中心);
|
||
// 用户刚手动滚动过(6 秒内)则不打断阅读,改由"回到诵读处"按钮兜底
|
||
const active = segEls.find((el) => Number(el.dataset.index) === index);
|
||
const userBusy = playing && Date.now() - lastUserScroll < 6000;
|
||
if (active && !userBusy) {
|
||
const target = active.querySelector(".ch.cur") || active;
|
||
target.scrollIntoView({ behavior: reduceMotionMQ.matches ? "auto" : "smooth", block: "center", inline: "center" });
|
||
}
|
||
requestAnimationFrame(updateJumpNow);
|
||
}
|
||
|
||
/** 诵读光标是否在经文视区内;不在则显示"回到诵读处"浮动按钮。 */
|
||
function updateJumpNow() {
|
||
if (!playing || currentSeg < 0) {
|
||
btnJumpNow.hidden = true;
|
||
return;
|
||
}
|
||
const active = segEls.find((el) => Number(el.dataset.index) === currentSeg);
|
||
if (!active) {
|
||
btnJumpNow.hidden = false; // 当前段不在本页,提示可跳回
|
||
return;
|
||
}
|
||
const target = active.querySelector(".ch.cur") || active;
|
||
const r = target.getBoundingClientRect();
|
||
const c = sutraEl.getBoundingClientRect();
|
||
btnJumpNow.hidden = r.top >= c.top && r.bottom <= c.bottom;
|
||
}
|
||
|
||
function clearHighlight() {
|
||
segEls.forEach((el) => {
|
||
el.classList.remove("playing", "done");
|
||
el.querySelectorAll(".ch").forEach((s) => s.classList.remove("read", "cur"));
|
||
delete el.dataset.cursorN;
|
||
});
|
||
dots.forEach((d) => d.classList.remove("now", "done"));
|
||
if (cursorRaf) {
|
||
cancelAnimationFrame(cursorRaf);
|
||
cursorRaf = null;
|
||
}
|
||
}
|
||
|
||
const reduceMotionMQ = matchMedia("(prefers-reduced-motion: reduce)");
|
||
|
||
// ===== 渲染 =====
|
||
function renderDots() {
|
||
progressEl.innerHTML = "";
|
||
dots = segments.map((_, i) => {
|
||
const d = document.createElement("span");
|
||
d.className = "dot";
|
||
d.title = `第 ${i + 1} 段,点击起读`;
|
||
d.addEventListener("click", () => playFrom(i));
|
||
progressEl.appendChild(d);
|
||
return d;
|
||
});
|
||
}
|
||
|
||
/** 渲染当前页的经文分段(含品名标题)。 */
|
||
function renderPage(page) {
|
||
currentPage = page;
|
||
sutraEl.innerHTML = "";
|
||
const start = page * sutra.pageSize;
|
||
const end = Math.min(start + sutra.pageSize, segments.length);
|
||
segEls = [];
|
||
|
||
for (let i = start; i < end; i += 1) {
|
||
const seg = segments[i];
|
||
if (seg.h) {
|
||
const h = document.createElement("h4");
|
||
h.className = "chap";
|
||
h.innerHTML = "";
|
||
const name = document.createElement("span");
|
||
name.className = "chap-name";
|
||
name.textContent = seg.h;
|
||
h.appendChild(name);
|
||
if (seg.g) {
|
||
const gloss = document.createElement("span");
|
||
gloss.className = "chap-gloss";
|
||
gloss.textContent = seg.g;
|
||
h.appendChild(gloss);
|
||
}
|
||
sutraEl.appendChild(h);
|
||
}
|
||
const p = document.createElement("p");
|
||
p.className = "seg";
|
||
p.dataset.index = i;
|
||
p.textContent = seg.t;
|
||
p.setAttribute("role", "button");
|
||
p.setAttribute("tabindex", "0");
|
||
const label = seg.h ? seg.h : `第 ${i + 1} 段`;
|
||
p.setAttribute("aria-label", `朗读:${label}`);
|
||
p.addEventListener("click", () => playFrom(i));
|
||
p.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter" || e.key === " ") {
|
||
e.preventDefault();
|
||
playFrom(i);
|
||
}
|
||
});
|
||
sutraEl.appendChild(p);
|
||
segEls.push(p);
|
||
}
|
||
|
||
sutraEl.scrollTop = 0;
|
||
sutraEl.scrollLeft = 0;
|
||
updatePager();
|
||
}
|
||
|
||
function updatePager() {
|
||
const multi = pageCount > 1;
|
||
pagerEl.hidden = !multi;
|
||
pageInfo.textContent = `第 ${currentPage + 1} / ${pageCount} 页`;
|
||
btnPrev.disabled = currentPage === 0;
|
||
btnNext.disabled = currentPage === pageCount - 1;
|
||
}
|
||
|
||
/** 手动翻页(停止当前朗读)。 */
|
||
function gotoPage(page) {
|
||
if (page < 0 || page >= pageCount || page === currentPage) return;
|
||
stopPlayback();
|
||
renderPage(page);
|
||
setStatus(`已翻到第 ${page + 1} / ${pageCount} 页`);
|
||
}
|
||
|
||
/** 切换经文。 */
|
||
function selectSutra(id) {
|
||
const next = SUTRAS.find((s) => s.id === id);
|
||
if (!next || next === sutra) return;
|
||
stopPlayback();
|
||
sutra = next;
|
||
savePref("sutra", id);
|
||
segments = sutra.segments;
|
||
pageCount = Math.ceil(segments.length / sutra.pageSize);
|
||
|
||
document.title = sutra.name;
|
||
document.getElementById("sutra-title").textContent = sutra.title;
|
||
document.getElementById("sutra-sub").textContent = sutra.translator;
|
||
const sealEl = document.getElementById("seal");
|
||
sealEl.textContent = sutra.seal;
|
||
sealEl.dataset.chars = sutra.seal.length;
|
||
const src = document.getElementById("sutra-source");
|
||
src.textContent = sutra.source;
|
||
src.href = sutra.sourceUrl;
|
||
|
||
sutraSelect.value = id;
|
||
sheetSyncFns.get(sutraSelect)?.();
|
||
|
||
renderDots();
|
||
renderPage(0);
|
||
setStatus(`已切换到《${sutra.name}》· 共 ${segments.length} 段${pageCount > 1 ? ` / ${pageCount} 页` : ""}`);
|
||
}
|
||
|
||
// ===== 播放控制 =====
|
||
/** 取消进行中的请求与播放,令牌递增使旧循环失效。 */
|
||
function stopPlayback() {
|
||
playToken += 1;
|
||
setPlayingUI(false);
|
||
btnPlay.classList.remove("preparing");
|
||
if (inflight) {
|
||
inflight.abort();
|
||
inflight = null;
|
||
}
|
||
audioEl.pause();
|
||
audioEl.ontimeupdate = null;
|
||
audioEl.onended = null;
|
||
stopBgm();
|
||
clearHighlight();
|
||
btnJumpNow.hidden = true;
|
||
currentSeg = -1;
|
||
}
|
||
|
||
/**
|
||
* 获取指定段的音频 Blob,命中缓存则直接返回。
|
||
* 失败自动重试(最多 3 次,间隔 1s/2s 递增),吸收服务重启等瞬时 502;
|
||
* 用户停止播放导致的 AbortError 不重试。
|
||
* @param {number} index 段序号
|
||
* @returns {Promise<Blob>} 音频数据
|
||
*/
|
||
async function fetchSegment(index) {
|
||
const key = `${sutra.id}|${currentVoice()}|${index}`;
|
||
if (audioCache.has(key)) return audioCache.get(key);
|
||
|
||
let lastErr = null;
|
||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||
if (attempt > 0) await new Promise((r) => setTimeout(r, attempt * 1000));
|
||
inflight = new AbortController();
|
||
try {
|
||
const resp = await fetch("/api/tts", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ text: segments[index].t, voice: currentVoice() }),
|
||
signal: inflight.signal,
|
||
});
|
||
if (!resp.ok) {
|
||
const err = await resp.json().catch(() => ({}));
|
||
throw new Error(err.error || `HTTP ${resp.status}`);
|
||
}
|
||
const blob = await resp.blob();
|
||
audioCache.set(key, blob);
|
||
return blob;
|
||
} catch (err) {
|
||
if (err.name === "AbortError") throw err; // 主动停止,不重试
|
||
lastErr = err;
|
||
} finally {
|
||
inflight = null;
|
||
}
|
||
}
|
||
throw lastErr;
|
||
}
|
||
|
||
/**
|
||
* 解析 WAV 文件头,返回 {rate, channels, pcm};格式不符返回 null。
|
||
* @param {ArrayBuffer} buf WAV 数据
|
||
*/
|
||
function parseWav(buf) {
|
||
const v = new DataView(buf);
|
||
const tag = (o) => v.getUint32(o, true); // 块 ID 按小端读
|
||
if (buf.byteLength < 44 || tag(0) !== 0x46464952) return null; // 'RIFF'
|
||
let off = 12;
|
||
let fmt = null;
|
||
let pcm = null;
|
||
while (off + 8 <= buf.byteLength) {
|
||
const id = tag(off);
|
||
const sz = v.getUint32(off + 4, true);
|
||
if (id === 0x20746d66) { // 'fmt '
|
||
fmt = { channels: v.getUint16(off + 10, true), rate: v.getUint32(off + 12, true), bits: v.getUint16(off + 22, true) };
|
||
} else if (id === 0x61746164) { // 'data'
|
||
// 流式编码的 WAV 可能写占位符长度(如 0x7fffffxx),按实际文件长度截断
|
||
const realLen = Math.min(sz, buf.byteLength - (off + 8));
|
||
pcm = new Uint8Array(buf, off + 8, realLen);
|
||
break;
|
||
}
|
||
off += 8 + sz + (sz % 2);
|
||
}
|
||
if (!fmt || !pcm || fmt.bits !== 16) return null;
|
||
return { rate: fmt.rate, channels: fmt.channels, pcm };
|
||
}
|
||
|
||
/**
|
||
* 将全部段落 PCM 合并为一个 WAV:段间 0.8s 静音、末尾 3s 静音(循环间隔)。
|
||
* 合并成单文件后整个诵读只播一个音频,无 JS 事件链,手机锁屏/后台也不中断。
|
||
* @param {Blob[]} blobs 各段音频
|
||
* @returns {Promise<{url: string, times: number[]}>} 音频地址与各段起始秒
|
||
*/
|
||
async function buildCombined(blobs) {
|
||
const parts = [];
|
||
const times = [];
|
||
let rate = 24000;
|
||
let channels = 1;
|
||
let cursor = 0; // 已写入字节数
|
||
for (const b of blobs) {
|
||
const w = parseWav(await b.arrayBuffer());
|
||
if (!w) throw new Error("音频格式不支持");
|
||
rate = w.rate;
|
||
channels = w.channels;
|
||
if (parts.length) {
|
||
const gap = new Uint8Array(Math.floor(rate * 0.8) * channels * 2);
|
||
parts.push(gap);
|
||
cursor += gap.length;
|
||
}
|
||
times.push(cursor / (rate * channels * 2));
|
||
parts.push(w.pcm);
|
||
cursor += w.pcm.length;
|
||
}
|
||
parts.push(new Uint8Array(Math.floor(rate * 3) * channels * 2)); // 尾部 3s 静音
|
||
const total = parts.reduce((s, p) => s + p.length, 0);
|
||
const out = new ArrayBuffer(44 + total);
|
||
const v = new DataView(out);
|
||
const wstr = (o, s) => { for (let k = 0; k < s.length; k += 1) v.setUint8(o + k, s.charCodeAt(k)); };
|
||
wstr(0, "RIFF"); v.setUint32(4, 36 + total, true); wstr(8, "WAVE");
|
||
wstr(12, "fmt "); v.setUint32(16, 16, true); v.setUint16(20, 1, true);
|
||
v.setUint16(22, channels, true); v.setUint32(24, rate, true);
|
||
v.setUint32(28, rate * channels * 2, true); v.setUint16(32, channels * 2, true); v.setUint16(34, 16, true);
|
||
wstr(36, "data"); v.setUint32(40, total, true);
|
||
let o = 44;
|
||
for (const p of parts) {
|
||
new Uint8Array(out, o, p.length).set(p);
|
||
o += p.length;
|
||
}
|
||
return { url: URL.createObjectURL(new Blob([out], { type: "audio/wav" })), times };
|
||
}
|
||
|
||
/** 把段落文字逐字包成 .ch span(仅当前诵读段需要,幂等)。 */
|
||
function wrapChars(el) {
|
||
if (el.dataset.wrapped) return;
|
||
const frag = document.createDocumentFragment();
|
||
for (const c of el.textContent) {
|
||
const s = document.createElement("span");
|
||
s.className = "ch";
|
||
s.textContent = c;
|
||
frag.appendChild(s);
|
||
}
|
||
el.innerHTML = "";
|
||
el.appendChild(frag);
|
||
el.dataset.wrapped = "1";
|
||
}
|
||
|
||
/**
|
||
* 逐字光标:按音频当前时间在当前段内按比例推进,
|
||
* 已读字染朱砂、当前字挂金色下划线光标。TTS 无逐字时间戳,按语速均匀估算。
|
||
*/
|
||
function cursorTick() {
|
||
if (!playing || !segTimes.length) {
|
||
cursorRaf = null;
|
||
return;
|
||
}
|
||
const t = audioEl.currentTime;
|
||
let i = segTimes.length - 1;
|
||
while (i > 0 && segTimes[i] > t) i -= 1;
|
||
const active = segEls.find((el) => Number(el.dataset.index) === i);
|
||
if (active) {
|
||
wrapChars(active);
|
||
const chs = active.querySelectorAll(".ch");
|
||
const segDur = (i + 1 < segTimes.length ? segTimes[i + 1] - 0.8 : audioEl.duration - 3) - segTimes[i];
|
||
const n = Math.min(chs.length, Math.floor(((t - segTimes[i]) / Math.max(segDur, 0.1)) * chs.length));
|
||
const prev = Number(active.dataset.cursorN ?? -1);
|
||
if (n !== prev) {
|
||
const lo = Math.max(0, Math.min(n, prev));
|
||
const hi = Math.max(n, prev);
|
||
for (let k = lo; k <= hi && k < chs.length; k += 1) chs[k].classList.toggle("read", k < n);
|
||
active.dataset.cursorN = n;
|
||
const old = active.querySelector(".ch.cur");
|
||
old?.classList.remove("cur");
|
||
if (n < chs.length) chs[n].classList.add("cur");
|
||
}
|
||
cursorTick._n = (cursorTick._n || 0) + 1;
|
||
if (cursorTick._n % 15 === 0) {
|
||
// 光标行跟随:光标走出视区 25%~75% 舒适带时,平滑滚回中部
|
||
if (Date.now() - lastUserScroll >= 6000 && chs[n]) {
|
||
const r = chs[n].getBoundingClientRect();
|
||
const c = sutraEl.getBoundingClientRect();
|
||
if (r.top < c.top + c.height * 0.25 || r.bottom > c.top + c.height * 0.75) {
|
||
chs[n].scrollIntoView({ behavior: reduceMotionMQ.matches ? "auto" : "smooth", block: "center", inline: "center" });
|
||
}
|
||
}
|
||
updateJumpNow();
|
||
}
|
||
}
|
||
cursorRaf = requestAnimationFrame(cursorTick);
|
||
}
|
||
|
||
/** 播放进度回调:按当前时间定位段号,高亮并自动翻页滚动;顺带判定定时停止。 */
|
||
function onTimeUpdate() {
|
||
if (sleepEndAt && Date.now() >= sleepEndAt) {
|
||
fireSleepTimer();
|
||
return;
|
||
}
|
||
const t = audioEl.currentTime;
|
||
let i = segTimes.length - 1;
|
||
while (i > 0 && segTimes[i] > t) i -= 1;
|
||
if (i === currentSeg) return;
|
||
currentSeg = i;
|
||
const targetPage = pageOf(i);
|
||
if (targetPage !== currentPage) renderPage(targetPage);
|
||
highlight(i);
|
||
}
|
||
|
||
/**
|
||
* 从第 index 段开始连续朗读到全文结束;开启循环时 audio.loop 原生回卷。
|
||
* 先并发补齐所有段音频再合并为单个 WAV 播放,中途无 JS 事件链衔接,
|
||
* 手机锁屏/后台不会中断;高亮靠 timeupdate 按 segTimes 时间轴定位。
|
||
*/
|
||
async function playFrom(index) {
|
||
stopPlayback();
|
||
const token = ++playToken;
|
||
setPlayingUI(true);
|
||
// 准备阶段按钮脉冲 + 文字提示(移动端没有状态栏,这条反馈很关键)
|
||
btnPlay.classList.add("preparing");
|
||
btnPlay.querySelector(".play-label").textContent = "准备中";
|
||
setStatus("正在准备音频…");
|
||
|
||
const blobs = new Array(segments.length);
|
||
let next = 0;
|
||
let done = 0;
|
||
const worker = async () => {
|
||
while (next < segments.length && token === playToken) {
|
||
const i = next;
|
||
next += 1;
|
||
blobs[i] = await fetchSegment(i);
|
||
done += 1;
|
||
setStatus(`正在准备音频 ${done} / ${segments.length}…`);
|
||
}
|
||
};
|
||
|
||
try {
|
||
await Promise.all([worker(), worker(), worker(), worker()]);
|
||
if (token !== playToken) return;
|
||
const built = await buildCombined(blobs);
|
||
if (token !== playToken) return;
|
||
segTimes = built.times;
|
||
currentSeg = -1;
|
||
audioEl.src = built.url;
|
||
audioEl.loop = loopMode;
|
||
audioEl.ontimeupdate = onTimeUpdate;
|
||
audioEl.onended = () => {
|
||
setPlayingUI(false);
|
||
setStatus("诵读完毕");
|
||
stopBgm();
|
||
clearHighlight();
|
||
};
|
||
audioEl.currentTime = built.times[index];
|
||
await audioEl.play();
|
||
btnPlay.classList.remove("preparing");
|
||
btnPlay.querySelector(".play-label").textContent = "停 止";
|
||
if (bgmOn) startBgm(); // play() 成功后起步背景音乐(用户手势上下文内)
|
||
updateMediaSession();
|
||
cursorRaf = requestAnimationFrame(cursorTick); // 启动逐字光标
|
||
setStatus(`正在朗读 · ${currentVoice()}`);
|
||
} catch (err) {
|
||
if (err.name === "AbortError") return; // 已被停止/新播放取代
|
||
setStatus(`语音合成失败:${err.message}`, true);
|
||
setPlayingUI(false);
|
||
btnPlay.classList.remove("preparing");
|
||
clearHighlight();
|
||
}
|
||
}
|
||
|
||
/** 设置循环开关状态并同步按钮外观(供点击与定时联动共用)。 */
|
||
function setLoopMode(on) {
|
||
loopMode = on;
|
||
audioEl.loop = on;
|
||
btnLoop.classList.toggle("active", on);
|
||
btnLoop.setAttribute("aria-pressed", String(on));
|
||
}
|
||
|
||
/** 循环诵读开关:开时读完全文自动回到第一段继续;定时关闭时即无限循环。 */
|
||
btnLoop.addEventListener("click", () => {
|
||
setLoopMode(!loopMode);
|
||
loopAutoByTimer = false; // 用户手动干预后以手动状态为准
|
||
savePref("loop", loopMode);
|
||
setStatus(loopMode ? "循环诵读:开" : "循环诵读:关");
|
||
});
|
||
|
||
/** 定时停止到期:停止播放、下拉复位,并收回定时自动带开的循环。 */
|
||
function fireSleepTimer() {
|
||
sleepEndAt = 0;
|
||
sleepTimer = null;
|
||
timerSelect.value = "0";
|
||
sheetSyncFns.get(timerSelect)?.();
|
||
if (loopAutoByTimer) {
|
||
loopAutoByTimer = false;
|
||
setLoopMode(false);
|
||
}
|
||
if (playing || inflight) {
|
||
stopPlayback();
|
||
setStatus("定时结束,已停止播放");
|
||
}
|
||
}
|
||
|
||
/** 定时停止:选 30/60/90/120 分钟后到点自动停播;选"关闭"取消。 */
|
||
timerSelect.addEventListener("change", () => {
|
||
if (sleepTimer) {
|
||
clearTimeout(sleepTimer);
|
||
sleepTimer = null;
|
||
}
|
||
const mins = Number(timerSelect.value);
|
||
if (!mins) {
|
||
sleepEndAt = 0;
|
||
if (loopAutoByTimer) {
|
||
// 定时带开的循环随"关闭"一并收回;用户手动开的循环不动
|
||
loopAutoByTimer = false;
|
||
setLoopMode(false);
|
||
}
|
||
setStatus("定时停止:关");
|
||
return;
|
||
}
|
||
sleepEndAt = Date.now() + mins * 60000;
|
||
sleepTimer = setTimeout(fireSleepTimer, mins * 60000);
|
||
if (!loopMode) {
|
||
setLoopMode(true); // 定时场景自动开启循环,播到点为止
|
||
loopAutoByTimer = true;
|
||
}
|
||
setStatus(`定时 ${mins} 分钟后停止 · 循环已开`);
|
||
});
|
||
|
||
/** 背景音乐开关:播放中开/关即时淡入淡出;未播放时只记录状态,下次播放生效。 */
|
||
btnBgm.addEventListener("click", () => {
|
||
bgmOn = !bgmOn;
|
||
btnBgm.classList.toggle("active", bgmOn);
|
||
btnBgm.setAttribute("aria-pressed", String(bgmOn));
|
||
savePref("bgm", bgmOn);
|
||
if (bgmOn && playing) startBgm();
|
||
if (!bgmOn) stopBgm();
|
||
setStatus(bgmOn ? "背景音乐:开" : "背景音乐:关");
|
||
});
|
||
|
||
/** 语速切换:直接改已合并音频的 playbackRate,立即生效。 */
|
||
rateSelect.addEventListener("change", () => {
|
||
audioEl.playbackRate = Number(rateSelect.value);
|
||
savePref("rate", rateSelect.value);
|
||
setStatus(`语速:${rateSelect.options[rateSelect.selectedIndex].text}`);
|
||
});
|
||
|
||
/** 夜间模式:暗色宣纸配色,睡前诵读不刺眼。 */
|
||
btnNight.addEventListener("click", () => {
|
||
const on = document.body.classList.toggle("night");
|
||
btnNight.classList.toggle("active", on);
|
||
btnNight.setAttribute("aria-pressed", String(on));
|
||
btnNight.textContent = on ? "日间" : "夜间";
|
||
savePref("night", on);
|
||
});
|
||
|
||
btnPlay.addEventListener("click", () => {
|
||
if (playing || inflight) {
|
||
stopPlayback();
|
||
setStatus("已停止");
|
||
} else {
|
||
playFrom(0);
|
||
}
|
||
});
|
||
|
||
voiceSelect.addEventListener("change", () => {
|
||
if (playing || inflight) stopPlayback();
|
||
savePref("voice", voiceSelect.value);
|
||
setStatus(`已切换音色:${voiceSelect.value},点击播放开始诵读`);
|
||
});
|
||
|
||
// 移动端设置区折叠/展开(齿轮图标按钮,桌面端不可见)
|
||
btnExpand.addEventListener("click", () => {
|
||
const expanded = playerCard.classList.toggle("expanded");
|
||
btnExpand.classList.toggle("open", expanded);
|
||
btnExpand.querySelector(".expand-label").textContent = expanded ? "收起" : "设置";
|
||
btnExpand.setAttribute("aria-expanded", String(expanded));
|
||
});
|
||
|
||
btnPrev.addEventListener("click", () => gotoPage(currentPage - 1));
|
||
btnNext.addEventListener("click", () => gotoPage(currentPage + 1));
|
||
|
||
// 手动滚轮/触摸滚动经文:记录时间,6 秒内跟读滚动让位给读者
|
||
["wheel", "touchmove"].forEach((ev) => {
|
||
sutraEl.addEventListener(ev, () => { lastUserScroll = Date.now(); }, { passive: true });
|
||
});
|
||
// 任何滚动(含自动滚动)都刷新"回到诵读处"按钮的可见性
|
||
sutraEl.addEventListener("scroll", () => { if (playing) updateJumpNow(); }, { passive: true });
|
||
btnJumpNow.addEventListener("click", () => {
|
||
lastUserScroll = 0;
|
||
let active = segEls.find((el) => Number(el.dataset.index) === currentSeg);
|
||
if (!active && currentSeg >= 0) {
|
||
renderPage(pageOf(currentSeg)); // 当前段不在本页,先翻过去
|
||
active = segEls.find((el) => Number(el.dataset.index) === currentSeg);
|
||
}
|
||
const target = active?.querySelector(".ch.cur") || active;
|
||
target?.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
|
||
btnJumpNow.hidden = true;
|
||
});
|
||
|
||
sutraSelect.addEventListener("change", () => selectSutra(sutraSelect.value));
|
||
|
||
// 键盘 ← → 翻页(下拉框聚焦时不拦截)
|
||
document.addEventListener("keydown", (e) => {
|
||
if (e.target.tagName === "SELECT") return;
|
||
if (e.key === "ArrowLeft") gotoPage(currentPage - 1);
|
||
if (e.key === "ArrowRight") gotoPage(currentPage + 1);
|
||
});
|
||
|
||
// ===== 字号与排版 =====
|
||
/** 应用字号档位并同步按钮高亮。 */
|
||
function setFontSize(size) {
|
||
if (!["sm", "md", "lg"].includes(size)) return;
|
||
document.querySelectorAll(".size-btn").forEach((b) => b.classList.toggle("active", b.dataset.size === size));
|
||
sutraEl.classList.remove("size-sm", "size-md", "size-lg");
|
||
sutraEl.classList.add(`size-${size}`);
|
||
}
|
||
document.querySelectorAll(".size-btn").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
setFontSize(btn.dataset.size);
|
||
savePref("size", btn.dataset.size);
|
||
});
|
||
});
|
||
|
||
const mobileMQ = matchMedia("(max-width: 640px)");
|
||
|
||
btnLayout.addEventListener("click", () => {
|
||
const vertical = sutraEl.classList.toggle("vertical");
|
||
btnLayout.textContent = vertical ? "切换横排" : "切换竖排";
|
||
btnLayout.classList.toggle("active", vertical);
|
||
});
|
||
|
||
// 移动端不提供竖排:窗口收窄时若处于竖排则切回横排
|
||
mobileMQ.addEventListener("change", () => {
|
||
if (mobileMQ.matches && sutraEl.classList.contains("vertical")) {
|
||
btnLayout.click();
|
||
}
|
||
});
|
||
|
||
// ===== 移动端底部升起选择器:原生 select 保留为状态源,手机上改由面板选择 =====
|
||
const sheetSyncFns = new Map(); // select → 同步按钮文字的函数
|
||
let sheetEls = null; // 当前打开的面板 {mask, panel}
|
||
|
||
/** 关闭底部选择面板(若开着)。 */
|
||
function closeSheet() {
|
||
if (!sheetEls) return;
|
||
const { mask, panel } = sheetEls;
|
||
sheetEls = null;
|
||
mask.classList.remove("open");
|
||
panel.classList.remove("open");
|
||
setTimeout(() => { mask.remove(); panel.remove(); }, 220);
|
||
}
|
||
|
||
/** 打开底部升起面板:列出 select 全部选项,点选后同步并派发 change。 */
|
||
function openSheet(select, title) {
|
||
closeSheet();
|
||
const mask = document.createElement("div");
|
||
mask.className = "sheet-mask";
|
||
const panel = document.createElement("div");
|
||
panel.className = "sheet-panel";
|
||
panel.setAttribute("role", "dialog");
|
||
panel.setAttribute("aria-label", title);
|
||
panel.innerHTML = `<div class="sheet-grabber"></div><div class="sheet-title">${title}</div><div class="sheet-opts"></div>`;
|
||
const opts = panel.querySelector(".sheet-opts");
|
||
let lastGroup = null; // optgroup 分组标题(如"佛经/道家"),无分组时不渲染
|
||
[...select.options].forEach((opt, i) => {
|
||
const group = opt.parentElement?.tagName === "OPTGROUP" ? opt.parentElement.label : null;
|
||
if (group && group !== lastGroup) {
|
||
lastGroup = group;
|
||
const gh = document.createElement("div");
|
||
gh.className = "sheet-group";
|
||
gh.textContent = group;
|
||
opts.appendChild(gh);
|
||
}
|
||
const item = document.createElement("div");
|
||
item.className = `sheet-opt${i === select.selectedIndex ? " sel" : ""}`;
|
||
item.textContent = opt.textContent;
|
||
item.setAttribute("role", "option");
|
||
item.addEventListener("click", () => {
|
||
select.value = opt.value;
|
||
select.dispatchEvent(new Event("change"));
|
||
closeSheet();
|
||
});
|
||
opts.appendChild(item);
|
||
});
|
||
const cancel = document.createElement("div");
|
||
cancel.className = "sheet-cancel";
|
||
cancel.textContent = "取 消";
|
||
cancel.addEventListener("click", closeSheet);
|
||
panel.appendChild(cancel);
|
||
mask.addEventListener("click", closeSheet);
|
||
document.body.appendChild(mask);
|
||
document.body.appendChild(panel);
|
||
sheetEls = { mask, panel };
|
||
requestAnimationFrame(() => {
|
||
mask.classList.add("open");
|
||
panel.classList.add("open");
|
||
});
|
||
}
|
||
|
||
/** 为 select 生成伪下拉按钮(仅移动端显示),点击打开底部面板。 */
|
||
function attachSheet(select, title) {
|
||
if (!select) return;
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "sheet-btn";
|
||
btn.setAttribute("aria-haspopup", "dialog");
|
||
const text = document.createElement("span");
|
||
text.className = "sheet-btn-text";
|
||
const sync = () => {
|
||
text.textContent = select.options[select.selectedIndex]?.textContent || "";
|
||
};
|
||
sync();
|
||
sheetSyncFns.set(select, sync);
|
||
select.addEventListener("change", sync);
|
||
btn.appendChild(text);
|
||
select.insertAdjacentElement("afterend", btn);
|
||
btn.addEventListener("click", () => openSheet(select, title));
|
||
}
|
||
attachSheet(sutraSelect, "选择经典");
|
||
attachSheet(voiceSelect, "诵读音色");
|
||
attachSheet(timerSelect, "定时停止");
|
||
attachSheet(rateSelect, "诵读语速");
|
||
|
||
// 锁屏/控制中心按键:上一段/下一段、暂停、停止
|
||
if ("mediaSession" in navigator) {
|
||
navigator.mediaSession.setActionHandler("play", () => { if (!playing && !inflight) playFrom(Math.max(currentSeg, 0)); });
|
||
navigator.mediaSession.setActionHandler("pause", () => stopPlayback());
|
||
navigator.mediaSession.setActionHandler("stop", () => stopPlayback());
|
||
navigator.mediaSession.setActionHandler("previoustrack", () => playFrom(Math.max(currentSeg - 1, 0)));
|
||
navigator.mediaSession.setActionHandler("nexttrack", () => { if (currentSeg + 1 < segments.length) playFrom(currentSeg + 1); });
|
||
}
|
||
|
||
// ===== PWA:注册 Service Worker,使"添加到主屏幕"后无地址栏独立运行 =====
|
||
if ("serviceWorker" in navigator) {
|
||
window.addEventListener("load", () => navigator.serviceWorker.register("/sw.js").catch(() => {}));
|
||
}
|
||
|
||
// ===== 初始化:恢复偏好(hash 锚点优先于记忆的经典/排版) =====
|
||
if (prefs.voice && [...voiceSelect.options].some((o) => o.value === prefs.voice)) voiceSelect.value = prefs.voice;
|
||
sheetSyncFns.get(voiceSelect)?.();
|
||
if (prefs.size) setFontSize(prefs.size);
|
||
if (prefs.loop) setLoopMode(true);
|
||
if (prefs.bgm) {
|
||
bgmOn = true;
|
||
btnBgm.classList.add("active");
|
||
btnBgm.setAttribute("aria-pressed", "true");
|
||
}
|
||
if (prefs.rate && [...rateSelect.options].some((o) => o.value === String(prefs.rate))) {
|
||
rateSelect.value = prefs.rate;
|
||
audioEl.playbackRate = Number(prefs.rate);
|
||
sheetSyncFns.get(rateSelect)?.();
|
||
}
|
||
if (prefs.night) {
|
||
document.body.classList.add("night");
|
||
btnNight.classList.add("active");
|
||
btnNight.setAttribute("aria-pressed", "true");
|
||
btnNight.textContent = "日间";
|
||
}
|
||
|
||
if (location.hash.includes("vertical") && !mobileMQ.matches) {
|
||
btnLayout.click();
|
||
}
|
||
// URL hash 可直达某部经(如 #jingang、#daodejing),优先于记忆的经典
|
||
const hashId = location.hash.replace(/^#/, "");
|
||
const initSutraId = SUTRAS.some((s) => s.id === hashId)
|
||
? hashId
|
||
: (SUTRAS.some((s) => s.id === prefs.sutra) ? prefs.sutra : "xinjing");
|
||
selectSutra(initSutraId);
|