Files
fj/app.js
T
selfreleaseandDevin bc0bfd9263 feat: 新增5部经典下拉选经、TTS重试、PWA可安装
- 新增阿弥陀经/普门品/道德经/清静经/太上感应篇(维基文库底本,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>
2026-10-04 15:47:18 +08:00

1422 lines
109 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 心经 / 金刚经 诵读页前端逻辑:经文渲染、分页阅读、逐段高亮朗读、音色选择。
*/
// ===== 经文数据 =====
// 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);